From 0809b888fc0f07a8b17e53890569c2573814bf16 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 12 Dec 2024 17:59:17 +0100 Subject: [PATCH 1/4] update import name for devtools vscode package (#12215) --- docs/source/development-testing/developer-tooling.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/development-testing/developer-tooling.mdx b/docs/source/development-testing/developer-tooling.mdx index 11e30ddfe8..7a43af3671 100644 --- a/docs/source/development-testing/developer-tooling.mdx +++ b/docs/source/development-testing/developer-tooling.mdx @@ -59,14 +59,14 @@ This feature is currently released as "experimental" - please try it out and giv ```sh npm install @apollo/client-devtools-vscode ``` -* After initializing your `ApolloClient` instance, call `registerClient` with your client instance. +* After initializing your `ApolloClient` instance, call `connectApolloClientToVSCodeDevTools` with your client instance. ```js -import { registerClient } from "@apollo/client-devtools-vscode"; +import { connectApolloClientToVSCodeDevTools } from "@apollo/client-devtools-vscode"; const client = new ApolloClient({ /* ... */ }); // we recommend wrapping this statement in a check for e.g. process.env.NODE_ENV === "development" -const devtoolsRegistration = registerClient( +const devtoolsRegistration = connectApolloClientToVSCodeDevTools( client, // the default port of the VSCode DevTools is 7095 "ws://localhost:7095", From 68a7b4a11ae89e8f19f0b7c76669289b9327009a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 12 Dec 2024 10:47:00 -0700 Subject: [PATCH 2/4] Remove `itAsync` part 1 (#12182) --- src/__tests__/ApolloClient.ts | 475 +- src/__tests__/client.ts | 4017 ++++++++--------- src/__tests__/local-state/general.ts | 1406 +++--- src/__tests__/local-state/resolvers.ts | 1450 +++--- .../inmemory/__tests__/fragmentMatcher.ts | 7 +- src/cache/inmemory/__tests__/writeToStore.ts | 423 +- src/core/__tests__/QueryManager/links.ts | 505 +-- src/core/__tests__/fetchPolicies.ts | 355 +- .../batch-http/__tests__/batchHttpLink.ts | 1093 ++--- src/link/batch/__tests__/batchLink.ts | 951 ++-- src/link/context/__tests__/index.ts | 247 +- src/link/error/__tests__/index.ts | 645 ++- src/link/http/__tests__/HttpLink.ts | 1804 ++++---- .../__tests__/persisted-queries.test.ts | 705 +-- src/link/ws/__tests__/webSocketLink.ts | 167 +- .../context/__tests__/ApolloConsumer.test.tsx | 11 +- .../__tests__/ssr/getDataFromTree.test.tsx | 153 +- .../hooks/__tests__/useMutation.test.tsx | 150 +- .../hooks/__tests__/useReactiveVar.test.tsx | 286 +- src/testing/internal/ObservableStream.ts | 15 +- src/testing/matchers/toEmitError.ts | 16 +- .../observables/__tests__/asyncMap.ts | 131 +- 22 files changed, 6862 insertions(+), 8150 deletions(-) diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index e147de233a..b7c1e8eb39 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -15,7 +15,6 @@ import { Observable } from "../utilities"; import { ApolloLink, FetchResult } from "../link/core"; import { HttpLink } from "../link/http"; import { createFragmentRegistry, InMemoryCache } from "../cache"; -import { itAsync } from "../testing"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { invariant } from "../utilities/globals"; @@ -1205,235 +1204,181 @@ describe("ApolloClient", () => { result.data?.people.friends[0].id; }); - itAsync( - "with a replacement of nested array (wq)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - const subscription = observable.subscribe({ - next(nextResult) { - ++count; - if (count === 1) { - expect(nextResult.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - - const readData = client.readQuery({ query }); - expect(readData).toEqual(data); - - // modify readData and writeQuery - const bestFriends = readData!.people.friends.filter( - (x) => x.type === "best" - ); - // this should re call next - client.writeQuery({ - query, - data: { - people: { - id: 1, - friends: bestFriends, - __typename: "Person", - }, - }, - }); - } else if (count === 2) { - const expectation = { - people: { - id: 1, - friends: [bestFriend], - __typename: "Person", - }, - }; - expect(nextResult.data).toEqual(expectation); - expect(client.readQuery({ query })).toEqual( - expectation - ); - subscription.unsubscribe(); - resolve(); - } + it("with a replacement of nested array (wq)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + expect(observable.getCurrentResult().data).toEqual(data); + + const readData = client.readQuery({ query }); + expect(readData).toEqual(data); + + // modify readData and writeQuery + const bestFriends = readData!.people.friends.filter( + (x) => x.type === "best" + ); + // this should re call next + client.writeQuery({ + query, + data: { + people: { + id: 1, + friends: bestFriends, + __typename: "Person", }, - }); - } - ); + }, + }); - itAsync( - "with a value change inside a nested array (wq)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - observable.subscribe({ - next: (nextResult) => { - count++; - if (count === 1) { - expect(nextResult.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - - const readData = client.readQuery({ query }); - expect(readData).toEqual(data); - - // modify readData and writeQuery - const friends = readData!.people.friends.slice(); - friends[0] = { ...friends[0], type: "okayest" }; - friends[1] = { ...friends[1], type: "okayest" }; - - // this should re call next - client.writeQuery({ - query, - data: { - people: { - id: 1, - friends, - __typename: "Person", - }, - }, - }); - - setTimeout(() => { - if (count === 1) - reject( - new Error( - "writeFragment did not re-call observable with next value" - ) - ); - }, 250); - } + const expectation = { + people: { + id: 1, + friends: [bestFriend], + __typename: "Person", + }, + }; - if (count === 2) { - const expectation0 = { - ...bestFriend, - type: "okayest", - }; - const expectation1 = { - ...badFriend, - type: "okayest", - }; - const nextFriends = nextResult.data!.people.friends; - expect(nextFriends[0]).toEqual(expectation0); - expect(nextFriends[1]).toEqual(expectation1); - - const readFriends = client.readQuery({ query })!.people - .friends; - expect(readFriends[0]).toEqual(expectation0); - expect(readFriends[1]).toEqual(expectation1); - resolve(); - } + await expect(stream).toEmitMatchedValue({ data: expectation }); + expect(client.readQuery({ query })).toEqual(expectation); + }); + + it("with a value change inside a nested array (wq)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + expect(observable.getCurrentResult().data).toEqual(data); + + const readData = client.readQuery({ query }); + expect(readData).toEqual(data); + + // modify readData and writeQuery + const friends = readData!.people.friends.slice(); + friends[0] = { ...friends[0], type: "okayest" }; + friends[1] = { ...friends[1], type: "okayest" }; + + // this should re call next + client.writeQuery({ + query, + data: { + people: { + id: 1, + friends, + __typename: "Person", }, - }); - } - ); + }, + }); + + const expectation0 = { + ...bestFriend, + type: "okayest", + }; + const expectation1 = { + ...badFriend, + type: "okayest", + }; + + const nextResult = await stream.takeNext(); + const nextFriends = nextResult.data!.people.friends; + + expect(nextFriends[0]).toEqual(expectation0); + expect(nextFriends[1]).toEqual(expectation1); + + const readFriends = client.readQuery({ query })!.people.friends; + expect(readFriends[0]).toEqual(expectation0); + expect(readFriends[1]).toEqual(expectation1); + }); }); + describe("using writeFragment", () => { - itAsync( - "with a replacement of nested array (wf)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - observable.subscribe({ - next: (result) => { - count++; - if (count === 1) { - expect(result.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - const bestFriends = result.data!.people.friends.filter( - (x) => x.type === "best" - ); - // this should re call next - client.writeFragment({ - id: `Person${result.data!.people.id}`, - fragment: gql` - fragment bestFriends on Person { - friends { - id - } - } - `, - data: { - friends: bestFriends, - __typename: "Person", - }, - }); - - setTimeout(() => { - if (count === 1) - reject( - new Error( - "writeFragment did not re-call observable with next value" - ) - ); - }, 50); - } + it("with a replacement of nested array (wf)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); - if (count === 2) { - expect(result.data!.people.friends).toEqual([bestFriend]); - resolve(); + { + const result = await stream.takeNext(); + + expect(result.data).toEqual(data); + expect(observable.getCurrentResult().data).toEqual(data); + + const bestFriends = result.data!.people.friends.filter( + (x) => x.type === "best" + ); + + // this should re call next + client.writeFragment({ + id: `Person${result.data!.people.id}`, + fragment: gql` + fragment bestFriends on Person { + friends { + id + } } + `, + data: { + friends: bestFriends, + __typename: "Person", }, }); } - ); - itAsync( - "with a value change inside a nested array (wf)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - observable.subscribe({ - next: (result) => { - count++; - if (count === 1) { - expect(result.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - const friends = result.data!.people.friends; - - // this should re call next - client.writeFragment({ - id: `Person${result.data!.people.id}`, - fragment: gql` - fragment bestFriends on Person { - friends { - id - type - } - } - `, - data: { - friends: [ - { ...friends[0], type: "okayest" }, - { ...friends[1], type: "okayest" }, - ], - __typename: "Person", - }, - }); - - setTimeout(() => { - if (count === 1) - reject( - new Error( - "writeFragment did not re-call observable with next value" - ) - ); - }, 50); - } + { + const result = await stream.takeNext(); + expect(result.data!.people.friends).toEqual([bestFriend]); + } + }); + + it("with a value change inside a nested array (wf)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); - if (count === 2) { - const nextFriends = result.data!.people.friends; - expect(nextFriends[0]).toEqual({ - ...bestFriend, - type: "okayest", - }); - expect(nextFriends[1]).toEqual({ - ...badFriend, - type: "okayest", - }); - resolve(); + { + const result = await stream.takeNext(); + + expect(result.data).toEqual(data); + expect(observable.getCurrentResult().data).toEqual(data); + const friends = result.data!.people.friends; + + // this should re call next + client.writeFragment({ + id: `Person${result.data!.people.id}`, + fragment: gql` + fragment bestFriends on Person { + friends { + id + type + } } + `, + data: { + friends: [ + { ...friends[0], type: "okayest" }, + { ...friends[1], type: "okayest" }, + ], + __typename: "Person", }, }); } - ); + + { + const result = await stream.takeNext(); + const nextFriends = result.data!.people.friends; + + expect(nextFriends[0]).toEqual({ + ...bestFriend, + type: "okayest", + }); + expect(nextFriends[1]).toEqual({ + ...badFriend, + type: "okayest", + }); + } + }); }); }); }); @@ -2804,69 +2749,63 @@ describe("ApolloClient", () => { invariantDebugSpy.mockRestore(); }); - itAsync( - "should catch refetchQueries error when not caught explicitly", - (resolve, reject) => { - const linkFn = jest - .fn( - () => - new Observable((observer) => { - setTimeout(() => { - observer.error(new Error("refetch failed")); - }); - }) - ) - .mockImplementationOnce(() => { - setTimeout(refetchQueries); - return Observable.of(); - }); - - const client = new ApolloClient({ - link: new ApolloLink(linkFn), - cache: new InMemoryCache(), + it("should catch refetchQueries error when not caught explicitly", (done) => { + expect.assertions(2); + const linkFn = jest + .fn( + () => + new Observable((observer) => { + setTimeout(() => { + observer.error(new Error("refetch failed")); + }); + }) + ) + .mockImplementationOnce(() => { + setTimeout(refetchQueries); + return Observable.of(); }); - const query = gql` - query someData { - foo { - bar - } + const client = new ApolloClient({ + link: new ApolloLink(linkFn), + cache: new InMemoryCache(), + }); + + const query = gql` + query someData { + foo { + bar } - `; + } + `; - const observable = client.watchQuery({ - query, - fetchPolicy: "network-only", - }); + const observable = client.watchQuery({ + query, + fetchPolicy: "network-only", + }); - observable.subscribe({}); + observable.subscribe({}); - function refetchQueries() { - const result = client.refetchQueries({ - include: "all", - }); + function refetchQueries() { + const result = client.refetchQueries({ + include: "all", + }); - result.queries[0].subscribe({ - error() { - setTimeout(() => { - try { - expect(invariantDebugSpy).toHaveBeenCalledTimes(1); - expect(invariantDebugSpy).toHaveBeenCalledWith( - "In client.refetchQueries, Promise.all promise rejected with error %o", - new ApolloError({ - networkError: new Error("refetch failed"), - }) - ); - resolve(); - } catch (err) { - reject(err); - } - }); - }, - }); - } + result.queries[0].subscribe({ + error() { + setTimeout(() => { + expect(invariantDebugSpy).toHaveBeenCalledTimes(1); + expect(invariantDebugSpy).toHaveBeenCalledWith( + "In client.refetchQueries, Promise.all promise rejected with error %o", + new ApolloError({ + networkError: new Error("refetch failed"), + }) + ); + done(); + }); + }, + }); } - ); + }); }); describe.skip("type tests", () => { diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index 686d1c078e..181e7b8d37 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -36,7 +36,7 @@ import { } from "../cache"; import { ApolloError } from "../errors"; -import { itAsync, mockSingleLink, MockLink, wait } from "../testing"; +import { mockSingleLink, MockLink, wait } from "../testing"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { waitFor } from "@testing-library/react"; @@ -106,36 +106,33 @@ describe("client", () => { ); }); - itAsync( - "should allow for a single query to take place", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - __typename - } + it("should allow for a single query to take place", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name __typename } + __typename } - `; + } + `; - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - __typename: "Person", - }, - ], - __typename: "People", - }, - }; + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + __typename: "Person", + }, + ], + __typename: "People", + }, + }; - return clientRoundtrip(resolve, reject, query, { data }); - } - ); + await clientRoundtrip(query, { data }); + }); it("should allow a single query with an apollo-link enabled network interface", async () => { const query = gql` @@ -176,137 +173,132 @@ describe("client", () => { expect(actualResult.data).toEqual(data); }); - itAsync( - "should allow for a single query with complex default variables to take place", - (resolve, reject) => { - const query = gql` - query stuff( - $test: Input = { key1: ["value", "value2"], key2: { key3: 4 } } - ) { - allStuff(test: $test) { - people { - name - } + it("should allow for a single query with complex default variables to take place", async () => { + const query = gql` + query stuff( + $test: Input = { key1: ["value", "value2"], key2: { key3: 4 } } + ) { + allStuff(test: $test) { + people { + name } } - `; + } + `; - const result = { - allStuff: { - people: [ - { - name: "Luke Skywalker", - }, - { - name: "Jabba The Hutt", - }, - ], - }, - }; + const result = { + allStuff: { + people: [ + { + name: "Luke Skywalker", + }, + { + name: "Jabba The Hutt", + }, + ], + }, + }; - const variables = { - test: { key1: ["value", "value2"], key2: { key3: 4 } }, - }; + const variables = { + test: { key1: ["value", "value2"], key2: { key3: 4 } }, + }; - const link = mockSingleLink({ - request: { query, variables }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query, variables }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const basic = client.query({ query, variables }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); + { + const actualResult = await client.query({ query, variables }); - const withDefault = client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); + expect(actualResult.data).toEqual(result); + } - return Promise.all([basic, withDefault]).then(resolve, reject); + { + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); } - ); + }); - itAsync( - "should allow for a single query with default values that get overridden with variables", - (resolve, reject) => { - const query = gql` - query people($first: Int = 1) { - allPeople(first: $first) { - people { - name - } + it("should allow for a single query with default values that get overridden with variables", async () => { + const query = gql` + query people($first: Int = 1) { + allPeople(first: $first) { + people { + name } } - `; + } + `; - const variables = { first: 1 }; - const override = { first: 2 }; + const variables = { first: 1 }; + const override = { first: 2 }; - const result = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; + const result = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; - const overriddenResult = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - { - name: "Jabba The Hutt", - }, - ], - }, - }; + const overriddenResult = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + { + name: "Jabba The Hutt", + }, + ], + }, + }; - const link = mockSingleLink( - { - request: { query, variables }, - result: { data: result }, - }, - { - request: { query, variables: override }, - result: { data: overriddenResult }, - } - ).setOnError(reject); + const link = mockSingleLink( + { + request: { query, variables }, + result: { data: result }, + }, + { + request: { query, variables: override }, + result: { data: overriddenResult }, + } + ); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const basic = client.query({ query, variables }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); + { + const actualResult = await client.query({ query, variables }); - const withDefault = client.query({ query }).then((actualResult) => { - return expect(actualResult.data).toEqual(result); - }); + expect(actualResult.data).toEqual(result); + } - const withOverride = client - .query({ query, variables: override }) - .then((actualResult) => { - return expect(actualResult.data).toEqual(overriddenResult); - }); + { + const actualResult = await client.query({ query }); - return Promise.all([basic, withDefault, withOverride]).then( - resolve, - reject - ); + expect(actualResult.data).toEqual(result); + } + + { + const actualResult = await client.query({ query, variables: override }); + + expect(actualResult.data).toEqual(overriddenResult); } - ); + }); - itAsync("should allow fragments on root query", (resolve, reject) => { + it("should allow fragments on root query", async () => { const query = gql` query { ...QueryFragment @@ -330,42 +322,39 @@ describe("client", () => { __typename: "Query", }; - return clientRoundtrip(resolve, reject, query, { data }, null); + return clientRoundtrip(query, { data }, null); }); - itAsync( - "should allow fragments on root query with ifm", - (resolve, reject) => { - const query = gql` - query { - ...QueryFragment - } + it("should allow fragments on root query with ifm", async () => { + const query = gql` + query { + ...QueryFragment + } - fragment QueryFragment on Query { - records { - id - name - __typename - } + fragment QueryFragment on Query { + records { + id + name __typename } - `; + __typename + } + `; - const data = { - records: [ - { id: 1, name: "One", __typename: "Record" }, - { id: 2, name: "Two", __typename: "Record" }, - ], - __typename: "Query", - }; + const data = { + records: [ + { id: 1, name: "One", __typename: "Record" }, + { id: 2, name: "Two", __typename: "Record" }, + ], + __typename: "Query", + }; - return clientRoundtrip(resolve, reject, query, { data }, null, { - Query: ["Record"], - }); - } - ); + await clientRoundtrip(query, { data }, null, { + Query: ["Record"], + }); + }); - itAsync("should merge fragments on root query", (resolve, reject) => { + it("should merge fragments on root query", async () => { // The fragment should be used after the selected fields for the query. // Otherwise, the results aren't merged. // see: https://github.com/apollographql/apollo-client/issues/1479 @@ -395,12 +384,12 @@ describe("client", () => { __typename: "Query", }; - return clientRoundtrip(resolve, reject, query, { data }, null, { + await clientRoundtrip(query, { data }, null, { Query: ["Record"], }); }); - itAsync("store can be rehydrated from the server", (resolve, reject) => { + it("store can be rehydrated from the server", async () => { const query = gql` query people { allPeople(first: 1) { @@ -424,7 +413,7 @@ describe("client", () => { const link = mockSingleLink({ request: { query }, result: { data }, - }).setOnError(reject); + }); const initialState: any = { data: { @@ -450,269 +439,238 @@ describe("client", () => { ), }); - return client - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - expect(finalState.data).toEqual( - (client.cache as InMemoryCache).extract() - ); - }) - .then(resolve, reject); + const result = await client.query({ query }); + + expect(result.data).toEqual(data); + expect(finalState.data).toEqual((client.cache as InMemoryCache).extract()); }); - itAsync( - "store can be rehydrated from the server using the shadow method", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("store can be rehydrated from the server using the shadow method", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); - const initialState: any = { - data: { - ROOT_QUERY: { - 'allPeople({"first":1})': { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, + const initialState: any = { + data: { + ROOT_QUERY: { + 'allPeople({"first":1})': { + people: [ + { + name: "Luke Skywalker", + }, + ], }, - optimistic: [], }, - }; + optimistic: [], + }, + }; - const finalState = assign({}, initialState, {}); + const finalState = assign({}, initialState, {}); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }).restore( - initialState.data - ), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }).restore( + initialState.data + ), + }); - return client - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - expect(finalState.data).toEqual(client.extract()); - }) - .then(resolve, reject); - } - ); + const result = await client.query({ query }); - itAsync( - "stores shadow of restore returns the same result as accessing the method directly on the cache", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `; + expect(result.data).toEqual(data); + expect(finalState.data).toEqual(client.extract()); + }); - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; - - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); + it("stores shadow of restore returns the same result as accessing the method directly on the cache", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; - const initialState: any = { - data: { - 'ROOT_QUERY.allPeople({"first":"1"}).people.0': { + const data = { + allPeople: { + people: [ + { name: "Luke Skywalker", }, - 'ROOT_QUERY.allPeople({"first":1})': { - people: [ - { - type: "id", - generated: true, - id: 'ROOT_QUERY.allPeople({"first":"1"}).people.0', - }, - ], - }, - ROOT_QUERY: { - 'allPeople({"first":1})': { + ], + }, + }; + + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); + + const initialState: any = { + data: { + 'ROOT_QUERY.allPeople({"first":"1"}).people.0': { + name: "Luke Skywalker", + }, + 'ROOT_QUERY.allPeople({"first":1})': { + people: [ + { type: "id", - id: 'ROOT_QUERY.allPeople({"first":1})', generated: true, + id: 'ROOT_QUERY.allPeople({"first":"1"}).people.0', }, + ], + }, + ROOT_QUERY: { + 'allPeople({"first":1})': { + type: "id", + id: 'ROOT_QUERY.allPeople({"first":1})', + generated: true, }, - optimistic: [], }, - }; - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }).restore( - initialState.data - ), - }); + optimistic: [], + }, + }; - expect(client.restore(initialState.data)).toEqual( - client.cache.restore(initialState.data) - ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }).restore( + initialState.data + ), + }); - resolve(); - } - ); + expect(client.restore(initialState.data)).toEqual( + client.cache.restore(initialState.data) + ); + }); - itAsync( - "should return errors correctly for a single query", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("should return errors correctly for a single query", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const errors: GraphQLError[] = [ - new GraphQLError( - "Syntax Error GraphQL request (8:9) Expected Name, found EOF" - ), - ]; + const errors: GraphQLError[] = [ + new GraphQLError( + "Syntax Error GraphQL request (8:9) Expected Name, found EOF" + ), + ]; - const link = mockSingleLink({ - request: { query }, - result: { errors }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query }, + result: { errors }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query }) - .catch((error: ApolloError) => { - expect(error.graphQLErrors).toEqual(errors); - }) - .then(resolve, reject); - } - ); + await expect(client.query({ query })).rejects.toEqual( + expect.objectContaining({ graphQLErrors: errors }) + ); + }); - itAsync( - "should return GraphQL errors correctly for a single query with an apollo-link enabled network interface", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("should return GraphQL errors correctly for a single query with an apollo-link enabled network interface", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; - const errors: GraphQLError[] = [ - new GraphQLError( - "Syntax Error GraphQL request (8:9) Expected Name, found EOF" - ), - ]; + const errors: GraphQLError[] = [ + new GraphQLError( + "Syntax Error GraphQL request (8:9) Expected Name, found EOF" + ), + ]; - const link = ApolloLink.from([ - () => { - return new Observable((observer) => { - observer.next({ data, errors }); - }); - }, - ]); + const link = ApolloLink.from([ + () => { + return new Observable((observer) => { + observer.next({ data, errors }); + }); + }, + ]); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - client.query({ query }).catch((error: ApolloError) => { - expect(error.graphQLErrors).toEqual(errors); - resolve(); - }); - } - ); + await expect(client.query({ query })).rejects.toEqual( + expect.objectContaining({ graphQLErrors: errors }) + ); + }); - itAsync( - "should pass a network error correctly on a query with apollo-link network interface", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("should pass a network error correctly on a query with apollo-link network interface", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const networkError = new Error("Some kind of network error."); + const networkError = new Error("Some kind of network error."); - const link = ApolloLink.from([ - () => { - return new Observable((_) => { - throw networkError; - }); - }, - ]); + const link = ApolloLink.from([ + () => { + return new Observable((_) => { + throw networkError; + }); + }, + ]); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - client.query({ query }).catch((error: ApolloError) => { - expect(error.networkError).toBeDefined(); - expect(error.networkError!.message).toEqual(networkError.message); - resolve(); - }); - } - ); + await expect(client.query({ query })).rejects.toThrow( + new ApolloError({ networkError }) + ); + }); it("should not warn when receiving multiple results from apollo-link network interface", () => { const query = gql` @@ -747,117 +705,22 @@ describe("client", () => { }); }); - itAsync.skip( - "should surface errors in observer.next as uncaught", - (resolve, reject) => { - const expectedError = new Error("this error should not reach the store"); - const listeners = process.listeners("uncaughtException"); - const oldHandler = listeners[listeners.length - 1]; - const handleUncaught = (e: Error) => { - console.log(e); - process.removeListener("uncaughtException", handleUncaught); - if (typeof oldHandler === "function") - process.addListener("uncaughtException", oldHandler); - if (e === expectedError) { - resolve(); - } else { - reject(e); - } - }; - process.removeListener("uncaughtException", oldHandler); - process.addListener("uncaughtException", handleUncaught); - - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `; - - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; - - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); - - const handle = client.watchQuery({ query }); - - handle.subscribe({ - next() { - throw expectedError; - }, - }); - } - ); - - itAsync.skip( - "should surfaces errors in observer.error as uncaught", - (resolve, reject) => { - const expectedError = new Error("this error should not reach the store"); - const listeners = process.listeners("uncaughtException"); - const oldHandler = listeners[listeners.length - 1]; - const handleUncaught = (e: Error) => { - process.removeListener("uncaughtException", handleUncaught); + it.skip("should surface errors in observer.next as uncaught", async () => { + const expectedError = new Error("this error should not reach the store"); + const listeners = process.listeners("uncaughtException"); + const oldHandler = listeners[listeners.length - 1]; + const handleUncaught = (e: Error) => { + console.log(e); + process.removeListener("uncaughtException", handleUncaught); + if (typeof oldHandler === "function") process.addListener("uncaughtException", oldHandler); - if (e === expectedError) { - resolve(); - } else { - reject(e); - } - }; - process.removeListener("uncaughtException", oldHandler); - process.addListener("uncaughtException", handleUncaught); - - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `; - - const link = mockSingleLink({ - request: { query }, - result: {}, - }).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); - - const handle = client.watchQuery({ query }); - handle.subscribe({ - next() { - reject(new Error("did not expect next to be called")); - }, - error() { - throw expectedError; - }, - }); - } - ); + if (e !== expectedError) { + throw e; + } + }; + process.removeListener("uncaughtException", oldHandler); + process.addListener("uncaughtException", handleUncaught); - itAsync("should allow for subscribing to a request", (resolve, reject) => { const query = gql` query people { allPeople(first: 1) { @@ -881,7 +744,7 @@ describe("client", () => { const link = mockSingleLink({ request: { query }, result: { data }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, @@ -891,37 +754,118 @@ describe("client", () => { const handle = client.watchQuery({ query }); handle.subscribe({ - next(result) { - expect(result.data).toEqual(data); - resolve(); + next() { + throw expectedError; }, }); }); - itAsync("should be able to transform queries", (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it.skip("should surfaces errors in observer.error as uncaught", async () => { + const expectedError = new Error("this error should not reach the store"); + const listeners = process.listeners("uncaughtException"); + const oldHandler = listeners[listeners.length - 1]; + const handleUncaught = (e: Error) => { + process.removeListener("uncaughtException", handleUncaught); + process.addListener("uncaughtException", oldHandler); + if (e !== expectedError) { + throw e; } - `; - const transformedQuery = gql` - query { - author { - firstName - lastName - __typename + }; + process.removeListener("uncaughtException", oldHandler); + process.addListener("uncaughtException", handleUncaught); + + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } } } `; - const result = { - author: { - firstName: "John", - lastName: "Smith", - }, + const link = mockSingleLink({ + request: { query }, + result: {}, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); + + const handle = client.watchQuery({ query }); + handle.subscribe({ + next() { + throw new Error("did not expect next to be called"); + }, + error() { + throw expectedError; + }, + }); + }); + + it("should allow for subscribing to a request", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; + + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; + + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); + + const handle = client.watchQuery({ query }); + const stream = new ObservableStream(handle); + + await expect(stream).toEmitMatchedValue({ data }); + }); + + it("should be able to transform queries", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const transformedQuery = gql` + query { + author { + firstName + lastName + __typename + } + } + `; + + const result = { + author: { + firstName: "John", + lastName: "Smith", + }, }; const transformedResult = { author: { @@ -941,79 +885,73 @@ describe("client", () => { result: { data: transformedResult }, }, false - ).setOnError(reject); + ); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: true }), }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(transformedResult); - }) - .then(resolve, reject); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(transformedResult); }); - itAsync( - "should be able to transform queries on network-only fetches", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should be able to transform queries on network-only fetches", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const transformedQuery = gql` - query { - author { - firstName - lastName - __typename - } + } + `; + const transformedQuery = gql` + query { + author { + firstName + lastName + __typename } - `; - const result = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const transformedResult = { - author: { - firstName: "John", - lastName: "Smith", - __typename: "Author", - }, - }; - const link = mockSingleLink( - { - request: { query }, - result: { data: result }, - }, - { - request: { query: transformedQuery }, - result: { data: transformedResult }, - }, - false - ).setOnError(reject); + } + `; + const result = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const transformedResult = { + author: { + firstName: "John", + lastName: "Smith", + __typename: "Author", + }, + }; + const link = mockSingleLink( + { + request: { query }, + result: { data: result }, + }, + { + request: { query: transformedQuery }, + result: { data: transformedResult }, + }, + false + ); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: true }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: true }), + }); - return client - .query({ fetchPolicy: "network-only", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(transformedResult); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ + fetchPolicy: "network-only", + query, + }); + + expect(actualResult.data).toEqual(transformedResult); + }); it("removes @client fields from the query before it reaches the link", async () => { const result: { current: Operation | undefined } = { @@ -1063,7 +1001,7 @@ describe("client", () => { expect(print(result.current!.query)).toEqual(print(transformedQuery)); }); - itAsync("should handle named fragments on mutations", (resolve, reject) => { + it("should handle named fragments on mutations", async () => { const mutation = gql` mutation { starAuthor(id: 12) { @@ -1091,117 +1029,64 @@ describe("client", () => { const link = mockSingleLink({ request: { query: mutation }, result: { data: result }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), }); - return client - .mutate({ mutation }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - }); - - itAsync( - "should be able to handle named fragments on network-only queries", - (resolve, reject) => { - const query = gql` - fragment authorDetails on Author { - firstName - lastName - } - - query { - author { - __typename - ...authorDetails - } - } - `; - const result = { - author: { - __typename: "Author", - firstName: "John", - lastName: "Smith", - }, - }; - - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); + const actualResult = await client.mutate({ mutation }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + expect(actualResult.data).toEqual(result); + }); - return client - .query({ fetchPolicy: "network-only", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + it("should be able to handle named fragments on network-only queries", async () => { + const query = gql` + fragment authorDetails on Author { + firstName + lastName + } - itAsync( - "should be able to handle named fragments with multiple fragments", - (resolve, reject) => { - const query = gql` - query { - author { - __typename - ...authorDetails - ...moreDetails - } + query { + author { + __typename + ...authorDetails } + } + `; + const result = { + author: { + __typename: "Author", + firstName: "John", + lastName: "Smith", + }, + }; - fragment authorDetails on Author { - firstName - lastName - } + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); - fragment moreDetails on Author { - address - } - `; - const result = { - author: { - __typename: "Author", - firstName: "John", - lastName: "Smith", - address: "1337 10th St.", - }, - }; + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const actualResult = await client.query({ + fetchPolicy: "network-only", + query, + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + expect(actualResult.data).toEqual(result); + }); - itAsync("should be able to handle named fragments", (resolve, reject) => { + it("should be able to handle named fragments with multiple fragments", async () => { const query = gql` query { author { __typename ...authorDetails + ...moreDetails } } @@ -1209,240 +1094,251 @@ describe("client", () => { firstName lastName } + + fragment moreDetails on Author { + address + } `; const result = { author: { __typename: "Author", firstName: "John", lastName: "Smith", + address: "1337 10th St.", }, }; const link = mockSingleLink({ request: { query }, result: { data: result }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - }); + const actualResult = await client.query({ query }); - itAsync( - "should be able to handle inlined fragments on an Interface type", - (resolve, reject) => { - const query = gql` - query items { - items { - ...ItemFragment - __typename - } - } + expect(actualResult.data).toEqual(result); + }); - fragment ItemFragment on Item { - id - __typename - ... on ColorItem { - color - __typename - } + it("should be able to handle named fragments", async () => { + const query = gql` + query { + author { + __typename + ...authorDetails } - `; - const result = { - items: [ - { - __typename: "ColorItem", - id: "27tlpoPeXm6odAxj3paGQP", - color: "red", - }, - { - __typename: "MonochromeItem", - id: "1t3iFLsHBm4c4RjOMdMgOO", - }, - ], - }; + } - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - possibleTypes: { - Item: ["ColorItem", "MonochromeItem"], - }, - }), - }); - return client - .query({ query }) - .then((actualResult: any) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + fragment authorDetails on Author { + firstName + lastName + } + `; + const result = { + author: { + __typename: "Author", + firstName: "John", + lastName: "Smith", + }, + }; - itAsync( - "should be able to handle inlined fragments on an Interface type with introspection fragment matcher", - (resolve, reject) => { - const query = gql` - query items { - items { - ...ItemFragment - __typename - } - } + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - fragment ItemFragment on Item { - id - ... on ColorItem { - color - __typename - } + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + }); + + it("should be able to handle inlined fragments on an Interface type", async () => { + const query = gql` + query items { + items { + ...ItemFragment __typename } - `; - const result = { - items: [ - { - __typename: "ColorItem", - id: "27tlpoPeXm6odAxj3paGQP", - color: "red", - }, - { - __typename: "MonochromeItem", - id: "1t3iFLsHBm4c4RjOMdMgOO", - }, - ], - }; + } - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); + fragment ItemFragment on Item { + id + __typename + ... on ColorItem { + color + __typename + } + } + `; + const result = { + items: [ + { + __typename: "ColorItem", + id: "27tlpoPeXm6odAxj3paGQP", + color: "red", + }, + { + __typename: "MonochromeItem", + id: "1t3iFLsHBm4c4RjOMdMgOO", + }, + ], + }; - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - possibleTypes: { - Item: ["ColorItem", "MonochromeItem"], - }, - }), - }); + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + possibleTypes: { + Item: ["ColorItem", "MonochromeItem"], + }, + }), + }); + const actualResult = await client.query({ query }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + expect(actualResult.data).toEqual(result); + }); - itAsync( - "should call updateQueries and update after mutation on query with inlined fragments on an Interface type", - (resolve, reject) => { - const query = gql` - query items { - items { - ...ItemFragment - __typename - } + it("should be able to handle inlined fragments on an Interface type with introspection fragment matcher", async () => { + const query = gql` + query items { + items { + ...ItemFragment + __typename } + } - fragment ItemFragment on Item { - id - ... on ColorItem { - color - __typename - } + fragment ItemFragment on Item { + id + ... on ColorItem { + color __typename } - `; - const result = { - items: [ - { - __typename: "ColorItem", - id: "27tlpoPeXm6odAxj3paGQP", - color: "red", - }, - { - __typename: "MonochromeItem", - id: "1t3iFLsHBm4c4RjOMdMgOO", - }, - ], - }; + __typename + } + `; + const result = { + items: [ + { + __typename: "ColorItem", + id: "27tlpoPeXm6odAxj3paGQP", + color: "red", + }, + { + __typename: "MonochromeItem", + id: "1t3iFLsHBm4c4RjOMdMgOO", + }, + ], + }; - const mutation = gql` - mutation myMutationName { - fortuneCookie + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + possibleTypes: { + Item: ["ColorItem", "MonochromeItem"], + }, + }), + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + }); + + it("should call updateQueries and update after mutation on query with inlined fragments on an Interface type", async () => { + const query = gql` + query items { + items { + ...ItemFragment + __typename } - `; - const mutationResult = { - fortuneCookie: "The waiter spit in your food", - }; + } - const link = mockSingleLink( + fragment ItemFragment on Item { + id + ... on ColorItem { + color + __typename + } + __typename + } + `; + const result = { + items: [ { - request: { query }, - result: { data: result }, + __typename: "ColorItem", + id: "27tlpoPeXm6odAxj3paGQP", + color: "red", }, { - request: { query: mutation }, - result: { data: mutationResult }, - } - ).setOnError(reject); + __typename: "MonochromeItem", + id: "1t3iFLsHBm4c4RjOMdMgOO", + }, + ], + }; - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - possibleTypes: { - Item: ["ColorItem", "MonochromeItem"], - }, - }), - }); + const mutation = gql` + mutation myMutationName { + fortuneCookie + } + `; + const mutationResult = { + fortuneCookie: "The waiter spit in your food", + }; - const queryUpdaterSpy = jest.fn(); - const queryUpdater = (prev: any) => { - queryUpdaterSpy(); - return prev; - }; - const updateQueries = { - items: queryUpdater, - }; + const link = mockSingleLink( + { + request: { query }, + result: { data: result }, + }, + { + request: { query: mutation }, + result: { data: mutationResult }, + } + ); - const updateSpy = jest.fn(); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + possibleTypes: { + Item: ["ColorItem", "MonochromeItem"], + }, + }), + }); - const obs = client.watchQuery({ query }); + const queryUpdaterSpy = jest.fn(); + const queryUpdater = (prev: any) => { + queryUpdaterSpy(); + return prev; + }; + const updateQueries = { + items: queryUpdater, + }; - const sub = obs.subscribe({ - next() { - client - .mutate({ mutation, updateQueries, update: updateSpy }) - .then(() => { - expect(queryUpdaterSpy).toBeCalled(); - expect(updateSpy).toBeCalled(); - sub.unsubscribe(); - resolve(); - }) - .catch((err) => { - reject(err); - }); - }, - error(err) { - reject(err); - }, - }); - } - ); + const updateSpy = jest.fn(); + + const obs = client.watchQuery({ query }); + const stream = new ObservableStream(obs); + + await expect(stream).toEmitNext(); + await client.mutate({ mutation, updateQueries, update: updateSpy }); + + expect(queryUpdaterSpy).toBeCalled(); + expect(updateSpy).toBeCalled(); + }); it("should send operationName along with the query to the server", () => { const query = gql` @@ -1494,61 +1390,7 @@ describe("client", () => { }); }); - itAsync( - "does not deduplicate queries if option is set to false", - (resolve, reject) => { - const queryDoc = gql` - query { - author { - name - } - } - `; - const data = { - author: { - name: "Jonas", - }, - }; - const data2 = { - author: { - name: "Dhaivat", - }, - }; - - // we have two responses for identical queries, and both should be requested. - // the second one should make it through to the network interface. - const link = mockSingleLink( - { - request: { query: queryDoc }, - result: { data }, - delay: 10, - }, - { - request: { query: queryDoc }, - result: { data: data2 }, - } - ).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - queryDeduplication: false, - }); - - const q1 = client.query({ query: queryDoc }); - const q2 = client.query({ query: queryDoc }); - - // if deduplication happened, result2.data will equal data. - return Promise.all([q1, q2]) - .then(([result1, result2]) => { - expect(result1.data).toEqual(data); - expect(result2.data).toEqual(data2); - }) - .then(resolve, reject); - } - ); - - itAsync("deduplicates queries by default", (resolve, reject) => { + it("does not deduplicate queries if option is set to false", async () => { const queryDoc = gql` query { author { @@ -1567,8 +1409,8 @@ describe("client", () => { }, }; - // we have two responses for identical queries, but only the first should be requested. - // the second one should never make it through to the network interface. + // we have two responses for identical queries, and both should be requested. + // the second one should make it through to the network interface. const link = mockSingleLink( { request: { query: queryDoc }, @@ -1579,24 +1421,25 @@ describe("client", () => { request: { query: queryDoc }, result: { data: data2 }, } - ).setOnError(reject); + ); + const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), + queryDeduplication: false, }); const q1 = client.query({ query: queryDoc }); const q2 = client.query({ query: queryDoc }); - // if deduplication didn't happen, result.data will equal data2. - return Promise.all([q1, q2]) - .then(([result1, result2]) => { - expect(result1.data).toEqual(result2.data); - }) - .then(resolve, reject); + // if deduplication happened, result2.data will equal data. + const [result1, result2] = await Promise.all([q1, q2]); + + expect(result1.data).toEqual(data); + expect(result2.data).toEqual(data2); }); - it("deduplicates queries if query context.queryDeduplication is set to true", () => { + it("deduplicates queries by default", async () => { const queryDoc = gql` query { author { @@ -1631,24 +1474,70 @@ describe("client", () => { const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), - queryDeduplication: false, }); - // Both queries need to be deduplicated, otherwise only one gets tracked - const q1 = client.query({ - query: queryDoc, - context: { queryDeduplication: true }, - }); - const q2 = client.query({ - query: queryDoc, - context: { queryDeduplication: true }, - }); + const q1 = client.query({ query: queryDoc }); + const q2 = client.query({ query: queryDoc }); - // if deduplication happened, result2.data will equal data. - return Promise.all([q1, q2]).then(([result1, result2]) => { - expect(result1.data).toEqual(data); - expect(result2.data).toEqual(data); - }); + // if deduplication didn't happen, result.data will equal data2. + const [result1, result2] = await Promise.all([q1, q2]); + + expect(result1.data).toEqual(result2.data); + }); + + it("deduplicates queries if query context.queryDeduplication is set to true", () => { + const queryDoc = gql` + query { + author { + name + } + } + `; + const data = { + author: { + name: "Jonas", + }, + }; + const data2 = { + author: { + name: "Dhaivat", + }, + }; + + // we have two responses for identical queries, but only the first should be requested. + // the second one should never make it through to the network interface. + const link = mockSingleLink( + { + request: { query: queryDoc }, + result: { data }, + delay: 10, + }, + { + request: { query: queryDoc }, + result: { data: data2 }, + } + ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + queryDeduplication: false, + }); + + // Both queries need to be deduplicated, otherwise only one gets tracked + const q1 = client.query({ + query: queryDoc, + context: { queryDeduplication: true }, + }); + const q2 = client.query({ + query: queryDoc, + context: { queryDeduplication: true }, + }); + + // if deduplication happened, result2.data will equal data. + return Promise.all([q1, q2]).then(([result1, result2]) => { + expect(result1.data).toEqual(data); + expect(result2.data).toEqual(data); + }); }); it("does not deduplicate queries if query context.queryDeduplication is set to false", () => { @@ -1702,53 +1591,53 @@ describe("client", () => { }); }); - itAsync( - "unsubscribes from deduplicated observables only once", - (resolve, reject) => { - const document: DocumentNode = gql` - query test1($x: String) { - test(x: $x) - } - `; + it("unsubscribes from deduplicated observables only once", async () => { + const document: DocumentNode = gql` + query test1($x: String) { + test(x: $x) + } + `; - const variables1 = { x: "Hello World" }; - const variables2 = { x: "Hello World" }; + const variables1 = { x: "Hello World" }; + const variables2 = { x: "Hello World" }; - let unsubscribed = false; + let unsubscribeCount = 0; - const client = new ApolloClient({ - link: new ApolloLink(() => { - return new Observable((observer) => { - observer.complete(); - return () => { - unsubscribed = true; - setTimeout(resolve, 0); - }; - }); - }), - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link: new ApolloLink(() => { + return new Observable((observer) => { + observer.complete(); + return () => { + unsubscribeCount++; + }; + }); + }), + cache: new InMemoryCache(), + }); - const sub1 = client - .watchQuery({ - query: document, - variables: variables1, - }) - .subscribe({}); + const sub1 = client + .watchQuery({ + query: document, + variables: variables1, + }) + .subscribe({}); - const sub2 = client - .watchQuery({ - query: document, - variables: variables2, - }) - .subscribe({}); + const sub2 = client + .watchQuery({ + query: document, + variables: variables2, + }) + .subscribe({}); - sub1.unsubscribe(); - expect(unsubscribed).toBe(false); + sub1.unsubscribe(); + // cleanup happens async + expect(unsubscribeCount).toBe(0); - sub2.unsubscribe(); - } - ); + sub2.unsubscribe(); + + await wait(0); + expect(unsubscribeCount).toBe(1); + }); describe("deprecated options", () => { const query = gql` @@ -1801,11 +1690,11 @@ describe("client", () => { }, }; - itAsync("for internal store", (resolve, reject) => { + it("for internal store", async () => { const link = mockSingleLink({ request: { query }, result: { data }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, @@ -1816,16 +1705,13 @@ describe("client", () => { }), }); - return client - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - expect((client.cache as InMemoryCache).extract()["1"]).toEqual({ - id: "1", - name: "Luke Skywalker", - }); - }) - .then(resolve, reject); + const result = await client.query({ query }); + + expect(result.data).toEqual(data); + expect((client.cache as InMemoryCache).extract()["1"]).toEqual({ + id: "1", + name: "Luke Skywalker", + }); }); }); @@ -1855,15 +1741,6 @@ describe("client", () => { "to receive multiple results from the cache and the network, or consider " + "using a different fetchPolicy, such as cache-first or network-only."; - function checkCacheAndNetworkError(callback: () => any) { - try { - callback(); - throw new Error("not reached"); - } catch (thrown) { - expect((thrown as Error).message).toBe(cacheAndNetworkError); - } - } - // Test that cache-and-network can only be used on watchQuery, not query. it("warns when used with client.query", () => { const client = new ApolloClient({ @@ -1871,12 +1748,12 @@ describe("client", () => { cache: new InMemoryCache(), }); - checkCacheAndNetworkError(() => - client.query({ + expect(() => { + void client.query({ query, fetchPolicy: "cache-and-network" as FetchPolicy, - }) - ); + }); + }).toThrow(new Error(cacheAndNetworkError)); }); it("warns when used with client.query with defaultOptions", () => { @@ -1890,14 +1767,15 @@ describe("client", () => { }, }); - checkCacheAndNetworkError(() => - client.query({ - query, - // This undefined value should be ignored in favor of - // defaultOptions.query.fetchPolicy. - fetchPolicy: void 0, - }) - ); + expect( + () => + void client.query({ + query, + // This undefined value should be ignored in favor of + // defaultOptions.query.fetchPolicy. + fetchPolicy: void 0, + }) + ).toThrow(new Error(cacheAndNetworkError)); }); it("fetches from cache first, then network", async () => { @@ -1950,7 +1828,7 @@ describe("client", () => { await expect(stream).not.toEmitAnything(); }); - itAsync("fails if network request fails", (resolve, reject) => { + it("fails if network request fails", async () => { const link = mockSingleLink(); // no queries = no replies. const client = new ApolloClient({ link, @@ -1961,59 +1839,42 @@ describe("client", () => { query, fetchPolicy: "cache-and-network", }); + const stream = new ObservableStream(obs); - obs.subscribe({ - error: (e) => { - if (!/No more mocked responses/.test(e.message)) { - reject(e); - } else { - resolve(); - } - }, - }); + const error = await stream.takeError(); + + expect(error.message).toMatch(/No more mocked responses/); }); - itAsync( - "fetches from cache first, then network and does not have an unhandled error", - (resolve, reject) => { - const link = mockSingleLink({ - request: { query }, - result: { errors: [{ message: "network failure" }] }, - }).setOnError(reject); + it("fetches from cache first, then network and does not have an unhandled error", async () => { + const link = mockSingleLink({ + request: { query }, + result: { errors: [{ message: "network failure" }] }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - client.writeQuery({ query, data: initialData }); + client.writeQuery({ query, data: initialData }); - const obs = client.watchQuery({ - query, - fetchPolicy: "cache-and-network", - }); - let shouldFail = true; - process.once("unhandledRejection", (rejection) => { - if (shouldFail) reject("promise had an unhandledRejection"); - }); - let count = 0; - obs.subscribe({ - next: (result) => { - expect(result.data).toEqual(initialData); - expect(result.loading).toBe(true); - count++; - }, - error: (e) => { - expect(e.message).toMatch(/network failure/); - expect(count).toBe(1); // make sure next was called. - setTimeout(() => { - shouldFail = false; - resolve(); - }, 0); - }, - }); - } - ); + const obs = client.watchQuery({ + query, + fetchPolicy: "cache-and-network", + }); + const stream = new ObservableStream(obs); + + await expect(stream).toEmitValue({ + loading: true, + data: initialData, + networkStatus: 1, + }); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/network failure/); + }); }); describe("standby queries", () => { @@ -2096,7 +1957,7 @@ describe("client", () => { }, }; - function makeLink(reject: (reason: any) => any) { + function makeLink() { return mockSingleLink( { request: { query }, @@ -2106,31 +1967,26 @@ describe("client", () => { request: { query }, result: { data: secondFetch }, } - ).setOnError(reject); + ); } - itAsync("forces the query to rerun", (resolve, reject) => { + it("forces the query to rerun", async () => { const client = new ApolloClient({ - link: makeLink(reject), + link: makeLink(), cache: new InMemoryCache({ addTypename: false }), }); // Run a query first to initialize the store - return ( - client - .query({ query }) - // then query for real - .then(() => client.query({ query, fetchPolicy: "network-only" })) - .then((result) => { - expect(result.data).toEqual({ myNumber: { n: 2 } }); - }) - .then(resolve, reject) - ); + await client.query({ query }); + // then query for real + const result = await client.query({ query, fetchPolicy: "network-only" }); + + expect(result.data).toEqual({ myNumber: { n: 2 } }); }); - itAsync("can be disabled with ssrMode", (resolve, reject) => { + it("can be disabled with ssrMode", async () => { const client = new ApolloClient({ - link: makeLink(reject), + link: makeLink(), ssrMode: true, cache: new InMemoryCache({ addTypename: false }), }); @@ -2138,185 +1994,76 @@ describe("client", () => { const options: QueryOptions = { query, fetchPolicy: "network-only" }; // Run a query first to initialize the store - return ( - client - .query({ query }) - // then query for real - .then(() => client.query(options)) - .then((result) => { - expect(result.data).toEqual({ myNumber: { n: 1 } }); - // Test that options weren't mutated, issue #339 - expect(options).toEqual({ - query, - fetchPolicy: "network-only", - }); - }) - .then(resolve, reject) - ); - }); + await client.query({ query }); + // then query for real + const result = await client.query(options); - itAsync( - "can temporarily be disabled with ssrForceFetchDelay", - (resolve, reject) => { - const client = new ApolloClient({ - link: makeLink(reject), - ssrForceFetchDelay: 100, - cache: new InMemoryCache({ addTypename: false }), - }); - - // Run a query first to initialize the store - return ( - client - .query({ query }) - // then query for real - .then(() => { - return client.query({ query, fetchPolicy: "network-only" }); - }) - .then(async (result) => { - expect(result.data).toEqual({ myNumber: { n: 1 } }); - await new Promise((resolve) => setTimeout(resolve, 100)); - return client.query({ query, fetchPolicy: "network-only" }); - }) - .then((result) => { - expect(result.data).toEqual({ myNumber: { n: 2 } }); - }) - .then(resolve, reject) - ); - } - ); - }); + expect(result.data).toEqual({ myNumber: { n: 1 } }); + // Test that options weren't mutated, issue #339 + expect(options).toEqual({ + query, + fetchPolicy: "network-only", + }); + }); - itAsync( - "should pass a network error correctly on a mutation", - (resolve, reject) => { - const mutation = gql` - mutation { - person { - firstName - lastName - } - } - `; - const data = { - person: { - firstName: "John", - lastName: "Smith", - }, - }; - const networkError = new Error("Some kind of network error."); + it("can temporarily be disabled with ssrForceFetchDelay", async () => { const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data }, - error: networkError, - }), + link: makeLink(), + ssrForceFetchDelay: 100, cache: new InMemoryCache({ addTypename: false }), }); - client - .mutate({ mutation }) - .then((_) => { - reject(new Error("Returned a result when it should not have.")); - }) - .catch((error: ApolloError) => { - expect(error.networkError).toBeDefined(); - expect(error.networkError!.message).toBe(networkError.message); - resolve(); + // Run a query first to initialize the store + await client.query({ query }); + // then query for real + { + const result = await client.query({ + query, + fetchPolicy: "network-only", }); - } - ); - itAsync( - "should pass a GraphQL error correctly on a mutation", - (resolve, reject) => { - const mutation = gql` - mutation { - newPerson { - person { - firstName - lastName - } - } - } - `; - const data = { - person: { - firstName: "John", - lastName: "Smith", - }, - }; - const errors = [new Error("Some kind of GraphQL error.")]; - const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data, errors }, - }).setOnError(reject), - cache: new InMemoryCache({ addTypename: false }), - }); - client - .mutate({ mutation }) - .then((_) => { - reject(new Error("Returned a result when it should not have.")); - }) - .catch((error: ApolloError) => { - expect(error.graphQLErrors).toBeDefined(); - expect(error.graphQLErrors.length).toBe(1); - expect(error.graphQLErrors[0].message).toBe(errors[0].message); - resolve(); - }); - } - ); + expect(result.data).toEqual({ myNumber: { n: 1 } }); + } - itAsync( - "should allow errors to be returned from a mutation", - (resolve, reject) => { - const mutation = gql` - mutation { - newPerson { - person { - firstName - lastName - } - } + await wait(100); + + const result = await client.query({ query, fetchPolicy: "network-only" }); + + expect(result.data).toEqual({ myNumber: { n: 2 } }); + }); + }); + + it("should pass a network error correctly on a mutation", async () => { + const mutation = gql` + mutation { + person { + firstName + lastName } - `; - const data = { - person: { - firstName: "John", - lastName: "Smith", - }, - }; - const errors = [new Error("Some kind of GraphQL error.")]; - const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { - errors, - data: { - newPerson: data, - }, - }, - }).setOnError(reject), - cache: new InMemoryCache({ addTypename: false }), - }); - client - .mutate({ mutation, errorPolicy: "all" }) - .then((result) => { - expect(result.errors).toBeDefined(); - expect(result.errors!.length).toBe(1); - expect(result.errors![0].message).toBe(errors[0].message); - expect(result.data).toEqual({ - newPerson: data, - }); - resolve(); - }) - .catch((error: ApolloError) => { - throw error; - }); - } - ); + } + `; + const data = { + person: { + firstName: "John", + lastName: "Smith", + }, + }; + const networkError = new Error("Some kind of network error."); + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data }, + error: networkError, + }), + cache: new InMemoryCache({ addTypename: false }), + }); + + await expect(client.mutate({ mutation })).rejects.toThrow( + new ApolloError({ networkError }) + ); + }); - itAsync("should strip errors on a mutation if ignored", (resolve, reject) => { + it("should pass a GraphQL error correctly on a mutation", async () => { const mutation = gql` mutation { newPerson { @@ -2328,11 +2075,9 @@ describe("client", () => { } `; const data = { - newPerson: { - person: { - firstName: "John", - lastName: "Smith", - }, + person: { + firstName: "John", + lastName: "Smith", }, }; const errors = [new Error("Some kind of GraphQL error.")]; @@ -2340,80 +2085,144 @@ describe("client", () => { link: mockSingleLink({ request: { query: mutation }, result: { data, errors }, - }).setOnError(reject), + }), cache: new InMemoryCache({ addTypename: false }), }); - client - .mutate({ mutation, errorPolicy: "ignore" }) - .then((result) => { - expect(result.errors).toBeUndefined(); - expect(result.data).toEqual(data); - resolve(); - }) - .catch((error: ApolloError) => { - throw error; - }); + + await expect(client.mutate({ mutation })).rejects.toEqual( + expect.objectContaining({ graphQLErrors: errors }) + ); }); - itAsync( - "should rollback optimistic after mutation got a GraphQL error", - (resolve, reject) => { - const mutation = gql` - mutation { - newPerson { - person { - firstName - lastName - } - } - } - `; - const data = { - newPerson: { - person: { - firstName: "John", - lastName: "Smith", + it("should allow errors to be returned from a mutation", async () => { + const mutation = gql` + mutation { + newPerson { + person { + firstName + lastName + } + } + } + `; + const data = { + person: { + firstName: "John", + lastName: "Smith", + }, + }; + const errors = [new Error("Some kind of GraphQL error.")]; + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { + errors, + data: { + newPerson: data, }, }, - }; - const errors = [new Error("Some kind of GraphQL error.")]; - const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data, errors }, - }).setOnError(reject), - cache: new InMemoryCache({ addTypename: false }), - }); - const mutatePromise = client.mutate({ - mutation, - optimisticResponse: { - newPerson: { - person: { - firstName: "John*", - lastName: "Smith*", - }, - }, + }), + cache: new InMemoryCache({ addTypename: false }), + }); + + const result = await client.mutate({ mutation, errorPolicy: "all" }); + + expect(result.errors).toBeDefined(); + expect(result.errors!.length).toBe(1); + expect(result.errors![0].message).toBe(errors[0].message); + expect(result.data).toEqual({ + newPerson: data, + }); + }); + + it("should strip errors on a mutation if ignored", async () => { + const mutation = gql` + mutation { + newPerson { + person { + firstName + lastName + } + } + } + `; + const data = { + newPerson: { + person: { + firstName: "John", + lastName: "Smith", }, - }); + }, + }; + const errors = [new Error("Some kind of GraphQL error.")]; + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data, errors }, + }), + cache: new InMemoryCache({ addTypename: false }), + }); - { - const { data, optimisticData } = client.cache as any; - expect(optimisticData).not.toBe(data); - expect(optimisticData.parent).toBe(data.stump); - expect(optimisticData.parent.parent).toBe(data); + const result = await client.mutate({ mutation, errorPolicy: "ignore" }); + + expect(result.errors).toBeUndefined(); + expect(result.data).toEqual(data); + }); + + it("should rollback optimistic after mutation got a GraphQL error", async () => { + const mutation = gql` + mutation { + newPerson { + person { + firstName + lastName + } + } } + `; + const data = { + newPerson: { + person: { + firstName: "John", + lastName: "Smith", + }, + }, + }; + const errors = [new Error("Some kind of GraphQL error.")]; + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data, errors }, + }), + cache: new InMemoryCache({ addTypename: false }), + }); + const mutatePromise = client.mutate({ + mutation, + optimisticResponse: { + newPerson: { + person: { + firstName: "John*", + lastName: "Smith*", + }, + }, + }, + }); - mutatePromise - .then((_) => { - reject(new Error("Returned a result when it should not have.")); - }) - .catch((_: ApolloError) => { - const { data, optimisticData } = client.cache as any; - expect(optimisticData).toBe(data.stump); - resolve(); - }); + { + const { data, optimisticData } = client.cache as any; + expect(optimisticData).not.toBe(data); + expect(optimisticData.parent).toBe(data.stump); + expect(optimisticData.parent.parent).toBe(data); } - ); + + await expect(mutatePromise).rejects.toThrow(); + + { + const { data, optimisticData } = client.cache as any; + + expect(optimisticData).toBe(data.stump); + } + }); it("has a clearStore method which calls QueryManager", async () => { const client = new ApolloClient({ @@ -2526,100 +2335,83 @@ describe("client", () => { expect(count).toEqual(2); }); - itAsync( - "invokes onResetStore callbacks before notifying queries during resetStore call", - async (resolve, reject) => { - const delay = (time: number) => new Promise((r) => setTimeout(r, time)); + it("invokes onResetStore callbacks before notifying queries during resetStore call", async () => { + const delay = (time: number) => new Promise((r) => setTimeout(r, time)); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data = { - author: { - __typename: "Author", - firstName: "John", - lastName: "Smith", - }, - }; + const data = { + author: { + __typename: "Author", + firstName: "John", + lastName: "Smith", + }, + }; - const data2 = { - author: { - __typename: "Author", - firstName: "Joe", - lastName: "Joe", - }, - }; + const data2 = { + author: { + __typename: "Author", + firstName: "Joe", + lastName: "Joe", + }, + }; - const link = ApolloLink.from([ - new ApolloLink( - () => - new Observable((observer) => { - observer.next({ data }); - observer.complete(); - return; - }) - ), - ]); + const link = ApolloLink.from([ + new ApolloLink( + () => + new Observable((observer) => { + observer.next({ data }); + observer.complete(); + return; + }) + ), + ]); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - let count = 0; - const onResetStoreOne = jest.fn(async () => { - expect(count).toEqual(0); - await delay(10).then(() => count++); - expect(count).toEqual(1); - }); + let count = 0; + const onResetStoreOne = jest.fn(async () => { + expect(count).toEqual(0); + await delay(10).then(() => count++); + expect(count).toEqual(1); + }); - const onResetStoreTwo = jest.fn(async () => { - expect(count).toEqual(0); - await delay(11).then(() => count++); - expect(count).toEqual(2); - expect(client.readQuery({ query })).toBe(null); - client.cache.writeQuery({ query, data: data2 }); - }); + const onResetStoreTwo = jest.fn(async () => { + expect(count).toEqual(0); + await delay(11).then(() => count++); + expect(count).toEqual(2); + expect(client.readQuery({ query })).toBe(null); + client.cache.writeQuery({ query, data: data2 }); + }); - client.onResetStore(onResetStoreOne); - client.onResetStore(onResetStoreTwo); + client.onResetStore(onResetStoreOne); + client.onResetStore(onResetStoreTwo); - let called = false; - const next = jest.fn((d) => { - if (called) { - expect(onResetStoreOne).toHaveBeenCalled(); - } else { - expect(d.data).toEqual(data); - called = true; - } - }); + const observable = client.watchQuery({ + query, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - client - .watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }) - .subscribe({ - next, - error: reject, - complete: reject, - }); + expect(count).toBe(0); + await client.resetStore(); + expect(count).toBe(2); - expect(count).toEqual(0); - await client.resetStore(); - expect(count).toEqual(2); - //watchQuery should only receive data twice - expect(next).toHaveBeenCalledTimes(2); + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream).toEmitNext(); - resolve(); - } - ); + expect(onResetStoreOne).toHaveBeenCalled(); + }); it("has a reFetchObservableQueries method which calls QueryManager", async () => { const client = new ApolloClient({ @@ -2690,845 +2482,758 @@ describe("client", () => { expect(spy).toHaveBeenCalled(); }); - itAsync( - "should propagate errors from network interface to observers", - (resolve, reject) => { - const link = ApolloLink.from([ - () => - new Observable((x) => { - x.error(new Error("Uh oh!")); - return; - }), - ]); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + it("should propagate errors from network interface to observers", async () => { + const link = ApolloLink.from([ + () => + new Observable((x) => { + x.error(new Error("Uh oh!")); + return; + }), + ]); - const handle = client.watchQuery({ - query: gql` - query { - a - b - c - } - `, - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - handle.subscribe({ - error(error) { - expect(error.message).toBe("Uh oh!"); - resolve(); - }, - }); - } - ); - - itAsync( - "should be able to refetch after there was a network error", - (resolve, reject) => { - const query: DocumentNode = gql` - query somethingelse { - allPeople(first: 1) { - people { - name - } - } + const handle = client.watchQuery({ + query: gql` + query { + a + b + c } - `; + `, + }); - const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } }; - const dataTwo = { allPeople: { people: [{ name: "Princess Leia" }] } }; - const link = mockSingleLink( - { request: { query }, result: { data } }, - { request: { query }, error: new Error("This is an error!") }, - { request: { query }, result: { data: dataTwo } } - ); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const stream = new ObservableStream(handle); - let count = 0; - const noop = () => null; + const error = await stream.takeError(); - const observable = client.watchQuery({ - query, - notifyOnNetworkStatusChange: true, - }); + expect(error.message).toBe("Uh oh!"); + }); - let subscription: any = null; - - const observerOptions = { - next(result: any) { - try { - switch (count++) { - case 0: - if (!result.data!.allPeople) { - reject("Should have data by this point"); - break; - } - // First result is loaded, run a refetch to get the second result - // which is an error. - expect(result.loading).toBeFalsy(); - expect(result.networkStatus).toBe(7); - expect(result.data!.allPeople).toEqual(data.allPeople); - setTimeout(() => { - observable.refetch().then(() => { - reject("Expected error value on first refetch."); - }, noop); - }, 0); - break; - case 1: - // Waiting for the second result to load - expect(result.loading).toBeTruthy(); - expect(result.networkStatus).toBe(4); - break; - // case 2 is handled by the error callback - case 3: - expect(result.loading).toBeTruthy(); - expect(result.networkStatus).toBe(4); - expect(result.errors).toBeFalsy(); - break; - case 4: - // Third result's data is loaded - expect(result.loading).toBeFalsy(); - expect(result.networkStatus).toBe(7); - expect(result.errors).toBeFalsy(); - if (!result.data) { - reject("Should have data by this point"); - break; - } - expect(result.data.allPeople).toEqual(dataTwo.allPeople); - resolve(); - break; - default: - throw new Error("Unexpected fall through"); - } - } catch (e) { - reject(e); + it("should be able to refetch after there was a network error", async () => { + const query: DocumentNode = gql` + query somethingelse { + allPeople(first: 1) { + people { + name } - }, - error(error: Error) { - expect(count++).toBe(2); - expect(error.message).toBe("This is an error!"); - - subscription.unsubscribe(); - - const lastError = observable.getLastError(); - expect(lastError).toBeInstanceOf(ApolloError); - expect(lastError!.networkError).toEqual((error as any).networkError); - - const lastResult = observable.getLastResult(); - expect(lastResult).toBeTruthy(); - expect(lastResult!.loading).toBe(false); - expect(lastResult!.networkStatus).toBe(8); - - observable.resetLastResults(); - subscription = observable.subscribe(observerOptions); - - // The error arrived, run a refetch to get the third result - // which should now contain valid data. - setTimeout(() => { - observable.refetch().catch(() => { - reject("Expected good data on second refetch."); - }); - }, 0); - }, - }; - - subscription = observable.subscribe(observerOptions); - } - ); - - itAsync("should throw a GraphQL error", (resolve, reject) => { - const query = gql` - query { - posts { - foo - __typename } } `; - const errors: GraphQLError[] = [ - new GraphQLError('Cannot query field "foo" on type "Post".'), - ]; - const link = mockSingleLink({ - request: { query }, - result: { errors }, - }).setOnError(reject); + + const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } }; + const dataTwo = { allPeople: { people: [{ name: "Princess Leia" }] } }; + const link = mockSingleLink( + { request: { query }, result: { data } }, + { request: { query }, error: new Error("This is an error!") }, + { request: { query }, result: { data: dataTwo } } + ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); + + const observable = client.watchQuery({ + query, + notifyOnNetworkStatusChange: true, + }); + + let stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data, + }); + + await wait(0); + await expect(observable.refetch()).rejects.toThrow(); + + await expect(stream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.refetch, + data, + }); + + const error = await stream.takeError(); + + expect(error.message).toBe("This is an error!"); + + stream.unsubscribe(); + + const lastError = observable.getLastError(); + expect(lastError).toBeInstanceOf(ApolloError); + expect(lastError!.networkError).toEqual((error as any).networkError); + + const lastResult = observable.getLastResult(); + expect(lastResult).toBeTruthy(); + expect(lastResult!.loading).toBe(false); + expect(lastResult!.networkStatus).toBe(8); + + observable.resetLastResults(); + stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data, + }); + + await wait(0); + await expect(observable.refetch()).resolves.toBeTruthy(); + + await expect(stream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.refetch, + data, + }); + + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + errors: undefined, + data: dataTwo, + }); + + await expect(stream).not.toEmitAnything(); + }); + + it("should throw a GraphQL error", async () => { + const query = gql` + query { + posts { + foo + __typename + } + } + `; + const errors: GraphQLError[] = [ + new GraphQLError('Cannot query field "foo" on type "Post".'), + ]; + const link = mockSingleLink({ + request: { query }, + result: { errors }, + }); const client = new ApolloClient({ link, cache: new InMemoryCache(), }); - return client - .query({ query }) - .catch((err) => { - expect(err.message).toBe('Cannot query field "foo" on type "Post".'); - }) - .then(resolve, reject); + await expect(client.query({ query })).rejects.toThrow( + 'Cannot query field "foo" on type "Post".' + ); }); it("should warn if server returns wrong data", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query { - todos { - id - name - description - __typename - } + const query = gql` + query { + todos { + id + name + description + __typename } - `; - const result = { - data: { - todos: [ - { - id: "1", - name: "Todo 1", - price: 100, - __typename: "Todo", - }, - ], - }, - }; - - const link = mockSingleLink({ - request: { query }, - result, - }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - // Passing an empty map enables the warning: - possibleTypes: {}, - }), - }); + } + `; + const result = { + data: { + todos: [ + { + id: "1", + name: "Todo 1", + price: 100, + __typename: "Todo", + }, + ], + }, + }; - return client - .query({ query }) - .then(({ data }) => { - expect(data).toEqual(result.data); - }) - .then(resolve, reject); + const link = mockSingleLink({ + request: { query }, + result, + }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + // Passing an empty map enables the warning: + possibleTypes: {}, + }), }); + + const { data } = await client.query({ query }); + + expect(data).toEqual(result.data); }); - itAsync( - "runs a query with the connection directive and writes it to the store key defined in the directive", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) @connection(key: "abc") { - name - } + it("runs a query with the connection directive and writes it to the store key defined in the directive", async () => { + const query = gql` + { + books(skip: 0, limit: 2) @connection(key: "abc") { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query }); - itAsync( - "runs query with cache field policy analogous to @connection", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) { - name - } + expect(actualResult.data).toEqual(result); + }); + + it("runs query with cache field policy analogous to @connection", async () => { + const query = gql` + { + books(skip: 0, limit: 2) { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - books: { - keyArgs: () => "abc", - }, + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + books: { + keyArgs: () => "abc", }, }, }, - }), - }); + }, + }), + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query }); - itAsync( - "should remove the connection directive before the link is sent", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) @connection(key: "books") { - name - } + expect(actualResult.data).toEqual(result); + }); + + it("should remove the connection directive before the link is sent", async () => { + const query = gql` + { + books(skip: 0, limit: 2) @connection(key: "books") { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + }); }); describe("@connection", () => { - itAsync( - "should run a query with the @connection directive and write the result to the store key defined in the directive", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) @connection(key: "abc") { - name - } + it("should run a query with the @connection directive and write the result to the store key defined in the directive", async () => { + const query = gql` + { + books(skip: 0, limit: 2) @connection(key: "abc") { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); + + const actualResult = await client.query({ query }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); - }) - .then(resolve, reject); - } - ); + expect(actualResult.data).toEqual(result); + expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); + }); - itAsync( - "should run a query with the connection directive and filter arguments and write the result to the correct store key", - (resolve, reject) => { - const query = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) - @connection(key: "abc", filter: ["order"]) { - name - } + it("should run a query with the connection directive and filter arguments and write the result to the correct store key", async () => { + const query = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) + @connection(key: "abc", filter: ["order"]) { + name } - `; - const transformedQuery = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) { - name - __typename - } + } + `; + const transformedQuery = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const variables = { order: "popularity" }; + const variables = { order: "popularity" }; - const link = mockSingleLink({ - request: { query: transformedQuery, variables }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery, variables }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - return client - .query({ query, variables }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query, variables }); - itAsync( - "should support cache field policies that filter key arguments", - (resolve, reject) => { - const query = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) { - name - } + expect(actualResult.data).toEqual(result); + expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); + }); + + it("should support cache field policies that filter key arguments", async () => { + const query = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) { + name } - `; - const transformedQuery = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) { - name - __typename - } + } + `; + const transformedQuery = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) { + name + __typename } - `; - - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + } + `; - const variables = { order: "popularity" }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery, variables }, - result: { data: result }, - }).setOnError(reject); + const variables = { order: "popularity" }; - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - books: { - keyArgs: ["order"], - }, - }, - }, - }, - }), - }); + const link = mockSingleLink({ + request: { query: transformedQuery, variables }, + result: { data: result }, + }); - return client - .query({ query, variables }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); - }) - .then(resolve, reject); - } - ); - - itAsync( - "should broadcast changes for reactive variables", - async (resolve, reject) => { - const aVar = makeVar(123); - const bVar = makeVar("asdf"); - const cache: InMemoryCache = new InMemoryCache({ + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ typePolicies: { Query: { fields: { - a() { - return aVar(); - }, - b() { - return bVar(); + books: { + keyArgs: ["order"], }, }, }, }, - }); + }), + }); - const client = new ApolloClient({ cache }); + const actualResult = await client.query({ query, variables }); - const obsQueries = new Set>(); - const subs = new Set(); - function watch( - query: DocumentNode, - fetchPolicy: WatchQueryFetchPolicy = "cache-first" - ): any[] { - const results: any[] = []; - const obsQuery = client.watchQuery({ - query, - fetchPolicy, - }); - obsQueries.add(obsQuery); - subs.add( - obsQuery.subscribe({ - next(result) { - results.push(result.data); - }, - }) - ); - return results; - } + expect(actualResult.data).toEqual(result); + expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); + }); - const aResults = watch(gql` - { - a - } - `); - const bResults = watch(gql` - { - b - } - `); - const abResults = watch(gql` - { - a - b - } - `); + it("should broadcast changes for reactive variables", async () => { + const aVar = makeVar(123); + const bVar = makeVar("asdf"); + const cache: InMemoryCache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + a() { + return aVar(); + }, + b() { + return bVar(); + }, + }, + }, + }, + }); - await wait(); + const client = new ApolloClient({ cache }); - function checkLastResult( - results: any[], - expectedData: Record - ) { - const lastResult = results[results.length - 1]; - expect(lastResult).toEqual(expectedData); - return lastResult; - } - - checkLastResult(aResults, { a: 123 }); - const bAsdf = checkLastResult(bResults, { b: "asdf" }); - checkLastResult(abResults, { a: 123, b: "asdf" }); - - aVar(aVar() + 111); - await wait(); - - const a234 = checkLastResult(aResults, { a: 234 }); - expect(checkLastResult(bResults, { b: "asdf" })).toBe(bAsdf); - checkLastResult(abResults, { a: 234, b: "asdf" }); - - bVar(bVar().toUpperCase()); - await wait(); - - expect(checkLastResult(aResults, { a: 234 })).toBe(a234); - checkLastResult(bResults, { b: "ASDF" }); - checkLastResult(abResults, { a: 234, b: "ASDF" }); - - aVar(aVar() + 222); - bVar("oyez"); - await wait(); - - const a456 = checkLastResult(aResults, { a: 456 }); - const bOyez = checkLastResult(bResults, { b: "oyez" }); - const a456bOyez = checkLastResult(abResults, { a: 456, b: "oyez" }); - - // Since the ObservableQuery skips results that are the same as the - // previous result, and nothing is actually changing about the - // ROOT_QUERY.a field, clear previous results to give the invalidated - // results a chance to be delivered. - obsQueries.forEach((obsQuery) => obsQuery.resetLastResults()); - await wait(); - // Verify that resetting previous results did not trigger the delivery - // of any new results, by itself. - expect(checkLastResult(aResults, a456)).toBe(a456); - expect(checkLastResult(bResults, bOyez)).toBe(bOyez); - expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); - - // Now invalidate the ROOT_QUERY.a field. - client.cache.evict({ fieldName: "a" }); - await wait(); - - expect(checkLastResult(aResults, a456)).toBe(a456); - expect(checkLastResult(bResults, bOyez)).toBe(bOyez); - expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); - - const cQuery = gql` - { - c - } - `; - // Passing cache-only as the fetchPolicy allows the { c: "see" } - // result to be delivered even though networkStatus is still loading. - const cResults = watch(cQuery, "cache-only"); - - // Now try writing directly to the cache, rather than calling - // client.writeQuery. - client.cache.writeQuery({ - query: cQuery, - data: { - c: "see", - }, + const obsQueries = new Set>(); + const subs = new Set(); + function watch( + query: DocumentNode, + fetchPolicy: WatchQueryFetchPolicy = "cache-first" + ): any[] { + const results: any[] = []; + const obsQuery = client.watchQuery({ + query, + fetchPolicy, }); - await wait(); - - checkLastResult(aResults, a456); - checkLastResult(bResults, bOyez); - checkLastResult(abResults, a456bOyez); - checkLastResult(cResults, { c: "see" }); - - cache.modify({ - fields: { - c(value) { - expect(value).toBe("see"); - return "saw"; + obsQueries.add(obsQuery); + subs.add( + obsQuery.subscribe({ + next(result) { + results.push(result.data); }, - }, - }); - await wait(); + }) + ); + return results; + } - checkLastResult(aResults, a456); - checkLastResult(bResults, bOyez); - checkLastResult(abResults, a456bOyez); - checkLastResult(cResults, { c: "saw" }); + const aResults = watch(gql` + { + a + } + `); + const bResults = watch(gql` + { + b + } + `); + const abResults = watch(gql` + { + a + b + } + `); - client.cache.evict({ fieldName: "c" }); - await wait(); + await wait(); - checkLastResult(aResults, a456); - checkLastResult(bResults, bOyez); - checkLastResult(abResults, a456bOyez); - expect(checkLastResult(cResults, {})); + function checkLastResult( + results: any[], + expectedData: Record + ) { + const lastResult = results[results.length - 1]; + expect(lastResult).toEqual(expectedData); + return lastResult; + } - expect(aResults).toEqual([{ a: 123 }, { a: 234 }, { a: 456 }]); + checkLastResult(aResults, { a: 123 }); + const bAsdf = checkLastResult(bResults, { b: "asdf" }); + checkLastResult(abResults, { a: 123, b: "asdf" }); + + aVar(aVar() + 111); + await wait(); + + const a234 = checkLastResult(aResults, { a: 234 }); + expect(checkLastResult(bResults, { b: "asdf" })).toBe(bAsdf); + checkLastResult(abResults, { a: 234, b: "asdf" }); + + bVar(bVar().toUpperCase()); + await wait(); + + expect(checkLastResult(aResults, { a: 234 })).toBe(a234); + checkLastResult(bResults, { b: "ASDF" }); + checkLastResult(abResults, { a: 234, b: "ASDF" }); + + aVar(aVar() + 222); + bVar("oyez"); + await wait(); + + const a456 = checkLastResult(aResults, { a: 456 }); + const bOyez = checkLastResult(bResults, { b: "oyez" }); + const a456bOyez = checkLastResult(abResults, { a: 456, b: "oyez" }); + + // Since the ObservableQuery skips results that are the same as the + // previous result, and nothing is actually changing about the + // ROOT_QUERY.a field, clear previous results to give the invalidated + // results a chance to be delivered. + obsQueries.forEach((obsQuery) => obsQuery.resetLastResults()); + await wait(); + // Verify that resetting previous results did not trigger the delivery + // of any new results, by itself. + expect(checkLastResult(aResults, a456)).toBe(a456); + expect(checkLastResult(bResults, bOyez)).toBe(bOyez); + expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); + + // Now invalidate the ROOT_QUERY.a field. + client.cache.evict({ fieldName: "a" }); + await wait(); + + expect(checkLastResult(aResults, a456)).toBe(a456); + expect(checkLastResult(bResults, bOyez)).toBe(bOyez); + expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); + + const cQuery = gql` + { + c + } + `; + // Passing cache-only as the fetchPolicy allows the { c: "see" } + // result to be delivered even though networkStatus is still loading. + const cResults = watch(cQuery, "cache-only"); + + // Now try writing directly to the cache, rather than calling + // client.writeQuery. + client.cache.writeQuery({ + query: cQuery, + data: { + c: "see", + }, + }); + await wait(); - expect(bResults).toEqual([{ b: "asdf" }, { b: "ASDF" }, { b: "oyez" }]); + checkLastResult(aResults, a456); + checkLastResult(bResults, bOyez); + checkLastResult(abResults, a456bOyez); + checkLastResult(cResults, { c: "see" }); - expect(abResults).toEqual([ - { a: 123, b: "asdf" }, - { a: 234, b: "asdf" }, - { a: 234, b: "ASDF" }, - { a: 456, b: "oyez" }, - ]); + cache.modify({ + fields: { + c(value) { + expect(value).toBe("see"); + return "saw"; + }, + }, + }); + await wait(); - expect(cResults).toEqual([{}, { c: "see" }, { c: "saw" }, {}]); + checkLastResult(aResults, a456); + checkLastResult(bResults, bOyez); + checkLastResult(abResults, a456bOyez); + checkLastResult(cResults, { c: "saw" }); - subs.forEach((sub) => sub.unsubscribe()); + client.cache.evict({ fieldName: "c" }); + await wait(); - resolve(); - } - ); + checkLastResult(aResults, a456); + checkLastResult(bResults, bOyez); + checkLastResult(abResults, a456bOyez); + expect(checkLastResult(cResults, {})); + + expect(aResults).toEqual([{ a: 123 }, { a: 234 }, { a: 456 }]); + + expect(bResults).toEqual([{ b: "asdf" }, { b: "ASDF" }, { b: "oyez" }]); + + expect(abResults).toEqual([ + { a: 123, b: "asdf" }, + { a: 234, b: "asdf" }, + { a: 234, b: "ASDF" }, + { a: 456, b: "oyez" }, + ]); + + expect(cResults).toEqual([{}, { c: "see" }, { c: "saw" }, {}]); + + subs.forEach((sub) => sub.unsubscribe()); + }); function wait(time = 10) { return new Promise((resolve) => setTimeout(resolve, time)); } - itAsync( - "should call forgetCache for reactive vars when stopped", - async (resolve, reject) => { - const aVar = makeVar(123); - const bVar = makeVar("asdf"); - const aSpy = jest.spyOn(aVar, "forgetCache"); - const bSpy = jest.spyOn(bVar, "forgetCache"); - const cache: InMemoryCache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - a() { - return aVar(); - }, - b() { - return bVar(); - }, + it("should call forgetCache for reactive vars when stopped", async () => { + const aVar = makeVar(123); + const bVar = makeVar("asdf"); + const aSpy = jest.spyOn(aVar, "forgetCache"); + const bSpy = jest.spyOn(bVar, "forgetCache"); + const cache: InMemoryCache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + a() { + return aVar(); + }, + b() { + return bVar(); }, }, }, - }); - - const client = new ApolloClient({ cache }); + }, + }); - const obsQueries = new Set>(); - const subs = new Set(); - function watch( - query: DocumentNode, - fetchPolicy: WatchQueryFetchPolicy = "cache-first" - ): any[] { - const results: any[] = []; - const obsQuery = client.watchQuery({ - query, - fetchPolicy, - }); - obsQueries.add(obsQuery); - subs.add( - obsQuery.subscribe({ - next(result) { - results.push(result.data); - }, - }) - ); - return results; - } + const client = new ApolloClient({ cache }); - const aQuery = gql` - { - a - } - `; - const bQuery = gql` - { - b - } - `; - const abQuery = gql` - { - a - b - } - `; + const obsQueries = new Set>(); + const subs = new Set(); + function watch( + query: DocumentNode, + fetchPolicy: WatchQueryFetchPolicy = "cache-first" + ): any[] { + const results: any[] = []; + const obsQuery = client.watchQuery({ + query, + fetchPolicy, + }); + obsQueries.add(obsQuery); + subs.add( + obsQuery.subscribe({ + next(result) { + results.push(result.data); + }, + }) + ); + return results; + } - const aResults = watch(aQuery); - const bResults = watch(bQuery); + const aQuery = gql` + { + a + } + `; + const bQuery = gql` + { + b + } + `; + const abQuery = gql` + { + a + b + } + `; - expect(cache["watches"].size).toBe(2); + const aResults = watch(aQuery); + const bResults = watch(bQuery); - expect(aResults).toEqual([]); - expect(bResults).toEqual([]); + expect(cache["watches"].size).toBe(2); - expect(aSpy).not.toBeCalled(); - expect(bSpy).not.toBeCalled(); + expect(aResults).toEqual([]); + expect(bResults).toEqual([]); - subs.forEach((sub) => sub.unsubscribe()); + expect(aSpy).not.toBeCalled(); + expect(bSpy).not.toBeCalled(); - expect(aSpy).toBeCalledTimes(1); - expect(aSpy).toBeCalledWith(cache); - expect(bSpy).toBeCalledTimes(1); - expect(bSpy).toBeCalledWith(cache); + subs.forEach((sub) => sub.unsubscribe()); - expect(aResults).toEqual([]); - expect(bResults).toEqual([]); + expect(aSpy).toBeCalledTimes(1); + expect(aSpy).toBeCalledWith(cache); + expect(bSpy).toBeCalledTimes(1); + expect(bSpy).toBeCalledWith(cache); - expect(cache["watches"].size).toBe(0); - const abResults = watch(abQuery); - expect(abResults).toEqual([]); - expect(cache["watches"].size).toBe(1); + expect(aResults).toEqual([]); + expect(bResults).toEqual([]); - await wait(); + expect(cache["watches"].size).toBe(0); + const abResults = watch(abQuery); + expect(abResults).toEqual([]); + expect(cache["watches"].size).toBe(1); - expect(aResults).toEqual([]); - expect(bResults).toEqual([]); - expect(abResults).toEqual([{ a: 123, b: "asdf" }]); + await wait(); - client.stop(); + expect(aResults).toEqual([]); + expect(bResults).toEqual([]); + expect(abResults).toEqual([{ a: 123, b: "asdf" }]); - await wait(); + client.stop(); - expect(aSpy).toBeCalledTimes(2); - expect(aSpy).toBeCalledWith(cache); - expect(bSpy).toBeCalledTimes(2); - expect(bSpy).toBeCalledWith(cache); + await wait(); - resolve(); - } - ); + expect(aSpy).toBeCalledTimes(2); + expect(aSpy).toBeCalledWith(cache); + expect(bSpy).toBeCalledTimes(2); + expect(bSpy).toBeCalledWith(cache); + }); describe("default settings", () => { const query = gql` @@ -3777,12 +3482,12 @@ describe("@connection", () => { await expect(stream).not.toEmitAnything(); }); - itAsync("allows setting default options for query", (resolve, reject) => { + it("allows setting default options for query", async () => { const errors = [{ message: "failure", name: "failure" }]; const link = mockSingleLink({ request: { query }, result: { errors }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), @@ -3791,55 +3496,46 @@ describe("@connection", () => { }, }); - return client - .query({ query }) - .then((result) => { - expect(result.errors).toEqual(errors); - }) - .then(resolve, reject); + const result = await client.query({ query }); + + expect(result.errors).toEqual(errors); }); - itAsync( - "allows setting default options for mutation", - (resolve, reject) => { - const mutation = gql` - mutation upVote($id: ID!) { - upvote(id: $id) { - success - } + it("allows setting default options for mutation", async () => { + const mutation = gql` + mutation upVote($id: ID!) { + upvote(id: $id) { + success } - `; - - const data = { - upvote: { success: true }, - }; - - const link = mockSingleLink({ - request: { query: mutation, variables: { id: 1 } }, - result: { data }, - }).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - defaultOptions: { - mutate: { variables: { id: 1 } }, - }, - }); + } + `; - return client - .mutate({ - mutation, - // This undefined value should be ignored in favor of - // defaultOptions.mutate.variables. - variables: void 0, - }) - .then((result) => { - expect(result.data).toEqual(data); - }) - .then(resolve, reject); - } - ); + const data = { + upvote: { success: true }, + }; + + const link = mockSingleLink({ + request: { query: mutation, variables: { id: 1 } }, + result: { data }, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + defaultOptions: { + mutate: { variables: { id: 1 } }, + }, + }); + + const result = await client.mutate({ + mutation, + // This undefined value should be ignored in favor of + // defaultOptions.mutate.variables. + variables: void 0, + }); + + expect(result.data).toEqual(data); + }); }); }); @@ -6351,8 +6047,6 @@ describe("custom document transforms", () => { }); function clientRoundtrip( - resolve: (result: any) => any, - reject: (reason: any) => any, query: DocumentNode, data: FormattedExecutionResult, variables?: any, @@ -6361,7 +6055,7 @@ function clientRoundtrip( const link = mockSingleLink({ request: { query: cloneDeep(query) }, result: data, - }).setOnError(reject); + }); const client = new ApolloClient({ link, @@ -6370,10 +6064,7 @@ function clientRoundtrip( }), }); - return client - .query({ query, variables }) - .then((result) => { - expect(result.data).toEqual(data.data); - }) - .then(resolve, reject); + return client.query({ query, variables }).then((result) => { + expect(result.data).toEqual(data.data); + }); } diff --git a/src/__tests__/local-state/general.ts b/src/__tests__/local-state/general.ts index 0d65993e82..c1f570e85a 100644 --- a/src/__tests__/local-state/general.ts +++ b/src/__tests__/local-state/general.ts @@ -17,8 +17,7 @@ import { ApolloLink } from "../../link/core"; import { Operation } from "../../link/core"; import { ApolloClient } from "../../core"; import { ApolloCache, InMemoryCache } from "../../cache"; -import { itAsync } from "../../testing"; -import { spyOnConsole } from "../../testing/internal"; +import { ObservableStream, spyOnConsole } from "../../testing/internal"; describe("General functionality", () => { it("should not impact normal non-@client use", () => { @@ -279,57 +278,43 @@ describe("Cache manipulation", () => { }); }); - itAsync( - "should be able to write to the cache with a local mutation and have " + - "things rerender automatically", - (resolve, reject) => { - const query = gql` - { - field @client - } - `; + it("should be able to write to the cache with a local mutation and have things rerender automatically", async () => { + const query = gql` + { + field @client + } + `; - const mutation = gql` - mutation start { - start @client - } - `; + const mutation = gql` + mutation start { + start @client + } + `; - const resolvers = { - Query: { - field: () => 0, - }, - Mutation: { - start: (_1: any, _2: any, { cache }: { cache: InMemoryCache }) => { - cache.writeQuery({ query, data: { field: 1 } }); - return { start: true }; - }, + const resolvers = { + Query: { + field: () => 0, + }, + Mutation: { + start: (_1: any, _2: any, { cache }: { cache: InMemoryCache }) => { + cache.writeQuery({ query, data: { field: 1 } }); + return { start: true }; }, - }; + }, + }; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.empty(), - resolvers, - }); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + resolvers, + }); - let count = 0; - client.watchQuery({ query }).subscribe({ - next: ({ data }) => { - count++; - if (count === 1) { - expect({ ...data }).toMatchObject({ field: 0 }); - client.mutate({ mutation }); - } + const stream = new ObservableStream(client.watchQuery({ query })); - if (count === 2) { - expect({ ...data }).toMatchObject({ field: 1 }); - resolve(); - } - }, - }); - } - ); + await expect(stream).toEmitMatchedValue({ data: { field: 0 } }); + await client.mutate({ mutation }); + await expect(stream).toEmitMatchedValue({ data: { field: 1 } }); + }); it("should support writing to the cache with a local mutation using variables", () => { const query = gql` @@ -381,376 +366,352 @@ describe("Cache manipulation", () => { }); }); - itAsync( - "should read @client fields from cache on refetch (#4741)", - (resolve, reject) => { - const query = gql` - query FetchInitialData { - serverData { - id - title - } - selectedItemId @client + it("should read @client fields from cache on refetch (#4741)", async () => { + const query = gql` + query FetchInitialData { + serverData { + id + title } - `; + selectedItemId @client + } + `; - const mutation = gql` - mutation Select { - select(itemId: $id) @client - } - `; + const mutation = gql` + mutation Select { + select(itemId: $id) @client + } + `; - const serverData = { - __typename: "ServerData", - id: 123, - title: "Oyez and Onoz", - }; + const serverData = { + __typename: "ServerData", + id: 123, + title: "Oyez and Onoz", + }; - let selectedItemId = -1; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: new ApolloLink(() => Observable.of({ data: { serverData } })), - resolvers: { - Query: { - selectedItemId() { - return selectedItemId; - }, + let selectedItemId = -1; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink(() => Observable.of({ data: { serverData } })), + resolvers: { + Query: { + selectedItemId() { + return selectedItemId; }, - Mutation: { - select(_, { itemId }) { - selectedItemId = itemId; - }, + }, + Mutation: { + select(_, { itemId }) { + selectedItemId = itemId; }, }, - }); + }, + }); - client.watchQuery({ query }).subscribe({ - next(result) { - expect(result).toEqual({ - data: { - serverData, - selectedItemId, - }, - loading: false, - networkStatus: 7, - }); + const stream = new ObservableStream(client.watchQuery({ query })); - if (selectedItemId !== 123) { - client.mutate({ - mutation, - variables: { - id: 123, - }, - refetchQueries: ["FetchInitialData"], - }); - } else { - resolve(); - } - }, - }); - } - ); + await expect(stream).toEmitValue({ + data: { + serverData, + selectedItemId: -1, + }, + loading: false, + networkStatus: 7, + }); - itAsync( - "should rerun @client(always: true) fields on entity update", - (resolve, reject) => { - const query = gql` - query GetClientData($id: ID) { - clientEntity(id: $id) @client(always: true) { - id - title - titleLength @client(always: true) - } - } - `; + await client.mutate({ + mutation, + variables: { id: 123 }, + refetchQueries: ["FetchInitialData"], + }); - const mutation = gql` - mutation AddOrUpdate { - addOrUpdate(id: $id, title: $title) @client - } - `; + await expect(stream).toEmitValue({ + data: { + serverData, + selectedItemId: 123, + }, + loading: false, + networkStatus: 7, + }); + }); - const fragment = gql` - fragment ClientDataFragment on ClientData { + it("should rerun @client(always: true) fields on entity update", async () => { + const query = gql` + query GetClientData($id: ID) { + clientEntity(id: $id) @client(always: true) { id title + titleLength @client(always: true) } - `; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: new ApolloLink(() => Observable.of({ data: {} })), - resolvers: { - ClientData: { - titleLength(data) { - return data.title.length; - }, + } + `; + + const mutation = gql` + mutation AddOrUpdate { + addOrUpdate(id: $id, title: $title) @client + } + `; + + const fragment = gql` + fragment ClientDataFragment on ClientData { + id + title + } + `; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink(() => Observable.of({ data: {} })), + resolvers: { + ClientData: { + titleLength(data) { + return data.title.length; }, - Query: { - clientEntity(_root, { id }, { cache }) { - return cache.readFragment({ - id: cache.identify({ id, __typename: "ClientData" }), - fragment, - }); - }, + }, + Query: { + clientEntity(_root, { id }, { cache }) { + return cache.readFragment({ + id: cache.identify({ id, __typename: "ClientData" }), + fragment, + }); }, - Mutation: { - addOrUpdate(_root, { id, title }, { cache }) { - return cache.writeFragment({ - id: cache.identify({ id, __typename: "ClientData" }), - fragment, - data: { id, title, __typename: "ClientData" }, - }); - }, + }, + Mutation: { + addOrUpdate(_root, { id, title }, { cache }) { + return cache.writeFragment({ + id: cache.identify({ id, __typename: "ClientData" }), + fragment, + data: { id, title, __typename: "ClientData" }, + }); }, }, - }); + }, + }); - const entityId = 1; - const shortTitle = "Short"; - const longerTitle = "A little longer"; - client.mutate({ - mutation, - variables: { - id: entityId, - title: shortTitle, - }, + const entityId = 1; + const shortTitle = "Short"; + const longerTitle = "A little longer"; + await client.mutate({ + mutation, + variables: { + id: entityId, + title: shortTitle, + }, + }); + const stream = new ObservableStream( + client.watchQuery({ query, variables: { id: entityId } }) + ); + + { + const result = await stream.takeNext(); + + expect(result.data.clientEntity).toEqual({ + id: entityId, + title: shortTitle, + titleLength: shortTitle.length, + __typename: "ClientData", }); - let mutated = false; - client.watchQuery({ query, variables: { id: entityId } }).subscribe({ - next(result) { - if (!mutated) { - expect(result.data.clientEntity).toEqual({ - id: entityId, - title: shortTitle, - titleLength: shortTitle.length, - __typename: "ClientData", - }); - client.mutate({ - mutation, - variables: { - id: entityId, - title: longerTitle, - }, - }); - mutated = true; - } else if (mutated) { - expect(result.data.clientEntity).toEqual({ - id: entityId, - title: longerTitle, - titleLength: longerTitle.length, - __typename: "ClientData", - }); - resolve(); - } - }, + } + + await client.mutate({ + mutation, + variables: { + id: entityId, + title: longerTitle, + }, + }); + + { + const result = await stream.takeNext(); + + expect(result.data.clientEntity).toEqual({ + id: entityId, + title: longerTitle, + titleLength: longerTitle.length, + __typename: "ClientData", }); } - ); + + await expect(stream).not.toEmitAnything(); + }); }); describe("Sample apps", () => { - itAsync( - "should support a simple counter app using local state", - (resolve, reject) => { - const query = gql` - query GetCount { - count @client - lastCount # stored in db on server - } - `; + it("should support a simple counter app using local state", async () => { + const query = gql` + query GetCount { + count @client + lastCount # stored in db on server + } + `; - const increment = gql` - mutation Increment($amount: Int = 1) { - increment(amount: $amount) @client - } - `; + const increment = gql` + mutation Increment($amount: Int = 1) { + increment(amount: $amount) @client + } + `; - const decrement = gql` - mutation Decrement($amount: Int = 1) { - decrement(amount: $amount) @client - } - `; + const decrement = gql` + mutation Decrement($amount: Int = 1) { + decrement(amount: $amount) @client + } + `; - const link = new ApolloLink((operation) => { - expect(operation.operationName).toBe("GetCount"); - return Observable.of({ data: { lastCount: 1 } }); - }); + const link = new ApolloLink((operation) => { + expect(operation.operationName).toBe("GetCount"); + return Observable.of({ data: { lastCount: 1 } }); + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - resolvers: {}, - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + resolvers: {}, + }); - const update = ( - query: DocumentNode, - updater: (data: { count: number }, variables: { amount: number }) => any - ) => { - return ( - _result: {}, - variables: { amount: number }, - { cache }: { cache: ApolloCache } - ): null => { - const read = client.readQuery<{ count: number }>({ - query, - variables, - }); - if (read) { - const data = updater(read, variables); - cache.writeQuery({ query, variables, data }); - } else { - throw new Error("readQuery returned a falsy value"); - } - return null; - }; + const update = ( + query: DocumentNode, + updater: (data: { count: number }, variables: { amount: number }) => any + ) => { + return ( + _result: {}, + variables: { amount: number }, + { cache }: { cache: ApolloCache } + ): null => { + const read = client.readQuery<{ count: number }>({ + query, + variables, + }); + if (read) { + const data = updater(read, variables); + cache.writeQuery({ query, variables, data }); + } else { + throw new Error("readQuery returned a falsy value"); + } + return null; }; + }; - const resolvers = { - Query: { - count: () => 0, - }, - Mutation: { - increment: update(query, ({ count, ...rest }, { amount }) => ({ - ...rest, - count: count + amount, - })), - decrement: update(query, ({ count, ...rest }, { amount }) => ({ - ...rest, - count: count - amount, - })), - }, - }; + const resolvers = { + Query: { + count: () => 0, + }, + Mutation: { + increment: update(query, ({ count, ...rest }, { amount }) => ({ + ...rest, + count: count + amount, + })), + decrement: update(query, ({ count, ...rest }, { amount }) => ({ + ...rest, + count: count - amount, + })), + }, + }; - client.addResolvers(resolvers); - - let count = 0; - client.watchQuery({ query }).subscribe({ - next: ({ data }) => { - count++; - if (count === 1) { - try { - expect({ ...data }).toMatchObject({ count: 0, lastCount: 1 }); - } catch (e) { - reject(e); - } - client.mutate({ mutation: increment, variables: { amount: 2 } }); - } + client.addResolvers(resolvers); + const stream = new ObservableStream(client.watchQuery({ query })); - if (count === 2) { - try { - expect({ ...data }).toMatchObject({ count: 2, lastCount: 1 }); - } catch (e) { - reject(e); - } - client.mutate({ mutation: decrement, variables: { amount: 1 } }); - } - if (count === 3) { - try { - expect({ ...data }).toMatchObject({ count: 1, lastCount: 1 }); - } catch (e) { - reject(e); - } - resolve(); - } - }, - error: (e) => reject(e), - complete: reject, - }); - } - ); + await expect(stream).toEmitMatchedValue({ + data: { count: 0, lastCount: 1 }, + }); - itAsync( - "should support a simple todo app using local state", - (resolve, reject) => { - const query = gql` - query GetTasks { - todos @client { - message - title - } - } - `; + await client.mutate({ mutation: increment, variables: { amount: 2 } }); - const mutation = gql` - mutation AddTodo($message: String, $title: String) { - addTodo(message: $message, title: $title) @client - } - `; + await expect(stream).toEmitMatchedValue({ + data: { count: 2, lastCount: 1 }, + }); - const client = new ApolloClient({ - link: ApolloLink.empty(), - cache: new InMemoryCache(), - resolvers: {}, - }); + await client.mutate({ mutation: decrement, variables: { amount: 1 } }); + + await expect(stream).toEmitMatchedValue({ + data: { count: 1, lastCount: 1 }, + }); + }); - interface Todo { - title: string; - message: string; - __typename: string; + it("should support a simple todo app using local state", async () => { + const query = gql` + query GetTasks { + todos @client { + message + title + } } + `; - const update = ( - query: DocumentNode, - updater: (todos: any, variables: Todo) => any - ) => { - return ( - _result: {}, - variables: Todo, - { cache }: { cache: ApolloCache } - ): null => { - const data = updater( - client.readQuery({ query, variables }), - variables - ); - cache.writeQuery({ query, variables, data }); - return null; - }; - }; + const mutation = gql` + mutation AddTodo($message: String, $title: String) { + addTodo(message: $message, title: $title) @client + } + `; - const resolvers = { - Query: { - todos: () => [], - }, - Mutation: { - addTodo: update(query, ({ todos }, { title, message }: Todo) => ({ - todos: todos.concat([{ message, title, __typename: "Todo" }]), - })), - }, + const client = new ApolloClient({ + link: ApolloLink.empty(), + cache: new InMemoryCache(), + resolvers: {}, + }); + + interface Todo { + title: string; + message: string; + __typename: string; + } + + const update = ( + query: DocumentNode, + updater: (todos: any, variables: Todo) => any + ) => { + return ( + _result: {}, + variables: Todo, + { cache }: { cache: ApolloCache } + ): null => { + const data = updater(client.readQuery({ query, variables }), variables); + cache.writeQuery({ query, variables, data }); + return null; }; + }; - client.addResolvers(resolvers); - - let count = 0; - client.watchQuery({ query }).subscribe({ - next: ({ data }: any) => { - count++; - if (count === 1) { - expect({ ...data }).toMatchObject({ todos: [] }); - client.mutate({ - mutation, - variables: { - title: "Apollo Client 2.0", - message: "ship it", - }, - }); - } else if (count === 2) { - expect(data.todos.map((x: Todo) => ({ ...x }))).toMatchObject([ - { - title: "Apollo Client 2.0", - message: "ship it", - __typename: "Todo", - }, - ]); - resolve(); - } + const resolvers = { + Query: { + todos: () => [], + }, + Mutation: { + addTodo: update(query, ({ todos }, { title, message }: Todo) => ({ + todos: todos.concat([{ message, title, __typename: "Todo" }]), + })), + }, + }; + + client.addResolvers(resolvers); + const stream = new ObservableStream(client.watchQuery({ query })); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ todos: [] }); + } + + await client.mutate({ + mutation, + variables: { + title: "Apollo Client 2.0", + message: "ship it", + }, + }); + + { + const { data } = await stream.takeNext(); + + expect(data.todos).toEqual([ + { + title: "Apollo Client 2.0", + message: "ship it", + __typename: "Todo", }, - }); + ]); } - ); + }); }); describe("Combining client and server state/operations", () => { - itAsync("should merge remote and local state", (resolve, reject) => { + it("should merge remote and local state", async () => { const query = gql` query list { list(name: "my list") { @@ -815,462 +776,403 @@ describe("Combining client and server state/operations", () => { const observer = client.watchQuery({ query }); - let count = 0; - observer.subscribe({ - next: (response) => { - if (count === 0) { - const initial = { ...data }; - initial.list.items = initial.list.items.map((x) => ({ - ...x, - isSelected: false, - })); - expect(response.data).toMatchObject(initial); - } - if (count === 1) { - expect((response.data as any).list.items[0].isSelected).toBe(true); - expect((response.data as any).list.items[1].isSelected).toBe(false); - resolve(); + const stream = new ObservableStream(observer); + + { + const response = await stream.takeNext(); + const initial = { ...data }; + initial.list.items = initial.list.items.map((x) => ({ + ...x, + isSelected: false, + })); + + expect(response.data).toMatchObject(initial); + } + + await client.mutate({ + mutation: gql` + mutation SelectItem($id: Int!) { + toggleItem(id: $id) @client } - count++; - }, - error: reject, + `, + variables: { id: 1 }, }); - const variables = { id: 1 }; - const mutation = gql` - mutation SelectItem($id: Int!) { - toggleItem(id: $id) @client - } - `; - // After initial result, toggle the state of one of the items - setTimeout(() => { - client.mutate({ mutation, variables }); - }, 10); + + { + const response = await stream.takeNext(); + + expect((response.data as any).list.items[0].isSelected).toBe(true); + expect((response.data as any).list.items[1].isSelected).toBe(false); + } }); - itAsync( - "query resolves with loading: false if subsequent responses contain the same data", - (resolve, reject) => { - const request = { - query: gql` - query people($id: Int) { - people(id: $id) { - id - name - } + it("query resolves with loading: false if subsequent responses contain the same data", async () => { + const request = { + query: gql` + query people($id: Int) { + people(id: $id) { + id + name } - `, - variables: { - id: 1, - }, - notifyOnNetworkStatusChange: true, - }; + } + `, + variables: { + id: 1, + }, + notifyOnNetworkStatusChange: true, + }; - const PersonType = new GraphQLObjectType({ - name: "Person", - fields: { - id: { type: GraphQLID }, - name: { type: GraphQLString }, - }, - }); + const PersonType = new GraphQLObjectType({ + name: "Person", + fields: { + id: { type: GraphQLID }, + name: { type: GraphQLString }, + }, + }); - const peopleData = [ - { id: 1, name: "John Smith" }, - { id: 2, name: "Sara Smith" }, - { id: 3, name: "Budd Deey" }, - ]; - - const QueryType = new GraphQLObjectType({ - name: "Query", - fields: { - people: { - type: PersonType, - args: { - id: { - type: GraphQLInt, - }, - }, - resolve: (_, { id }) => { - return peopleData; + const peopleData = [ + { id: 1, name: "John Smith" }, + { id: 2, name: "Sara Smith" }, + { id: 3, name: "Budd Deey" }, + ]; + + const QueryType = new GraphQLObjectType({ + name: "Query", + fields: { + people: { + type: PersonType, + args: { + id: { + type: GraphQLInt, }, }, + resolve: (_, { id }) => { + return peopleData; + }, }, - }); + }, + }); - const schema = new GraphQLSchema({ query: QueryType }); - - const link = new ApolloLink((operation) => { - // @ts-ignore - return new Observable(async (observer) => { - const { query, operationName, variables } = operation; - try { - const result = await graphql({ - schema, - source: print(query), - variableValues: variables, - operationName, - }); - observer.next(result); - observer.complete(); - } catch (err) { - observer.error(err); - } - }); + const schema = new GraphQLSchema({ query: QueryType }); + + const link = new ApolloLink((operation) => { + // @ts-ignore + return new Observable(async (observer) => { + const { query, operationName, variables } = operation; + try { + const result = await graphql({ + schema, + source: print(query), + variableValues: variables, + operationName, + }); + observer.next(result); + observer.complete(); + } catch (err) { + observer.error(err); + } }); + }); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - }); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + }); - const observer = client.watchQuery(request); + const observable = client.watchQuery(request); + const stream = new ObservableStream(observable); - let count = 0; - observer.subscribe({ - next: ({ loading, data }) => { - if (count === 0) expect(loading).toBe(false); - if (count === 1) expect(loading).toBe(true); - if (count === 2) { - expect(loading).toBe(false); - resolve(); - } - count++; - }, - error: reject, - }); + await expect(stream).toEmitMatchedValue({ loading: false }); - setTimeout(() => { - observer.refetch({ - id: 2, - }); - }, 1); - } - ); + await observable.refetch({ id: 2 }); - itAsync( - "should correctly propagate an error from a client resolver", - async (resolve, reject) => { - const data = { - list: { - __typename: "List", - items: [ - { __typename: "ListItem", id: 1, name: "first", isDone: true }, - { __typename: "ListItem", id: 2, name: "second", isDone: false }, - ], - }, - }; + await expect(stream).toEmitMatchedValue({ loading: true }); + await expect(stream).toEmitMatchedValue({ loading: false }); + }); - const link = new ApolloLink(() => Observable.of({ data })); + it("should correctly propagate an error from a client resolver", async () => { + const data = { + list: { + __typename: "List", + items: [ + { __typename: "ListItem", id: 1, name: "first", isDone: true }, + { __typename: "ListItem", id: 2, name: "second", isDone: false }, + ], + }, + }; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Query: { - hasBeenIllegallyTouched: (_, _v, _c) => { - throw new Error("Illegal Query Operation Occurred"); - }, - }, + const link = new ApolloLink(() => Observable.of({ data })); - Mutation: { - touchIllegally: (_, _v, _c) => { - throw new Error("Illegal Mutation Operation Occurred"); - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Query: { + hasBeenIllegallyTouched: (_, _v, _c) => { + throw new Error("Illegal Query Operation Occurred"); }, }, - }); - const variables = { id: 1 }; - const query = gql` - query hasBeenIllegallyTouched($id: Int!) { - hasBeenIllegallyTouched(id: $id) @client - } - `; - const mutation = gql` - mutation SelectItem($id: Int!) { - touchIllegally(id: $id) @client - } - `; + Mutation: { + touchIllegally: (_, _v, _c) => { + throw new Error("Illegal Mutation Operation Occurred"); + }, + }, + }, + }); - try { - await client.query({ query, variables }); - reject("Should have thrown!"); - } catch (e) { - // Test Passed! - expect(() => { - throw e; - }).toThrowErrorMatchingSnapshot(); + const variables = { id: 1 }; + const query = gql` + query hasBeenIllegallyTouched($id: Int!) { + hasBeenIllegallyTouched(id: $id) @client } - - try { - await client.mutate({ mutation, variables }); - reject("Should have thrown!"); - } catch (e) { - // Test Passed! - expect(() => { - throw e; - }).toThrowErrorMatchingSnapshot(); + `; + const mutation = gql` + mutation SelectItem($id: Int!) { + touchIllegally(id: $id) @client } + `; - resolve(); - } - ); + await expect( + client.query({ query, variables }) + ).rejects.toThrowErrorMatchingSnapshot(); + + await expect( + client.mutate({ mutation, variables }) + ).rejects.toThrowErrorMatchingSnapshot(); + }); it("should handle a simple query with both server and client fields", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query GetCount { - count @client - lastCount - } - `; - const cache = new InMemoryCache(); + const query = gql` + query GetCount { + count @client + lastCount + } + `; + const cache = new InMemoryCache(); - const link = new ApolloLink((operation) => { - expect(operation.operationName).toBe("GetCount"); - return Observable.of({ data: { lastCount: 1 } }); - }); + const link = new ApolloLink((operation) => { + expect(operation.operationName).toBe("GetCount"); + return Observable.of({ data: { lastCount: 1 } }); + }); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - cache.writeQuery({ - query, - data: { - count: 0, - }, - }); + cache.writeQuery({ + query, + data: { + count: 0, + }, + }); - client.watchQuery({ query }).subscribe({ - next: ({ data }) => { - expect({ ...data }).toMatchObject({ count: 0, lastCount: 1 }); - resolve(); - }, - }); + const stream = new ObservableStream(client.watchQuery({ query })); + + await expect(stream).toEmitMatchedValue({ + data: { count: 0, lastCount: 1 }, }); }); it("should support nested querying of both server and client fields", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query GetUser { - user { - firstName @client - lastName - } + const query = gql` + query GetUser { + user { + firstName @client + lastName } - `; - - const cache = new InMemoryCache(); - const link = new ApolloLink((operation) => { - expect(operation.operationName).toBe("GetUser"); - return Observable.of({ - data: { - user: { - __typename: "User", - // We need an id (or a keyFields policy) because, if the User - // object is not identifiable, the call to cache.writeQuery - // below will simply replace the existing data rather than - // merging the new data with the existing data. - id: 123, - lastName: "Doe", - }, - }, - }); - }); - - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + } + `; - cache.writeQuery({ - query, + const cache = new InMemoryCache(); + const link = new ApolloLink((operation) => { + expect(operation.operationName).toBe("GetUser"); + return Observable.of({ data: { user: { __typename: "User", + // We need an id (or a keyFields policy) because, if the User + // object is not identifiable, the call to cache.writeQuery + // below will simply replace the existing data rather than + // merging the new data with the existing data. id: 123, - firstName: "John", + lastName: "Doe", }, }, }); + }); - client.watchQuery({ query }).subscribe({ - next({ data }: any) { - const { user } = data; - try { - expect(user).toMatchObject({ - firstName: "John", - lastName: "Doe", - __typename: "User", - }); - } catch (e) { - reject(e); - } - resolve(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + cache.writeQuery({ + query, + data: { + user: { + __typename: "User", + id: 123, + firstName: "John", }, - }); + }, + }); + + const stream = new ObservableStream(client.watchQuery({ query })); + + await expect(stream).toEmitMatchedValue({ + data: { + user: { + firstName: "John", + lastName: "Doe", + __typename: "User", + }, + }, }); }); - itAsync( - "should combine both server and client mutations", - (resolve, reject) => { - const query = gql` - query SampleQuery { - count @client - user { - firstName - } + it("should combine both server and client mutations", async () => { + const query = gql` + query SampleQuery { + count @client + user { + firstName } - `; + } + `; - const mutation = gql` - mutation SampleMutation { - incrementCount @client - updateUser(firstName: "Harry") { - firstName - } + const mutation = gql` + mutation SampleMutation { + incrementCount @client + updateUser(firstName: "Harry") { + firstName } - `; + } + `; - const counterQuery = gql` - { - count @client - } - `; + const counterQuery = gql` + { + count @client + } + `; - const userQuery = gql` - { - user { - firstName - } + const userQuery = gql` + { + user { + firstName } - `; + } + `; - let watchCount = 0; - const link = new ApolloLink((operation: Operation): Observable<{}> => { - if (operation.operationName === "SampleQuery") { - return Observable.of({ - data: { user: { __typename: "User", firstName: "John" } }, - }); - } - if (operation.operationName === "SampleMutation") { - return Observable.of({ - data: { updateUser: { __typename: "User", firstName: "Harry" } }, - }); - } + const link = new ApolloLink((operation: Operation): Observable<{}> => { + if (operation.operationName === "SampleQuery") { return Observable.of({ - errors: [new Error(`Unknown operation ${operation.operationName}`)], + data: { user: { __typename: "User", firstName: "John" } }, }); + } + if (operation.operationName === "SampleMutation") { + return Observable.of({ + data: { updateUser: { __typename: "User", firstName: "Harry" } }, + }); + } + return Observable.of({ + errors: [new Error(`Unknown operation ${operation.operationName}`)], }); + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: { - Mutation: { - incrementCount: (_, __, { cache }) => { - const { count } = cache.readQuery({ query: counterQuery }); - const data = { count: count + 1 }; - cache.writeQuery({ - query: counterQuery, - data, - }); - return null; - }, + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: { + Mutation: { + incrementCount: (_, __, { cache }) => { + const { count } = cache.readQuery({ query: counterQuery }); + const data = { count: count + 1 }; + cache.writeQuery({ + query: counterQuery, + data, + }); + return null; }, }, - }); + }, + }); - cache.writeQuery({ - query: counterQuery, - data: { - count: 0, - }, - }); + cache.writeQuery({ + query: counterQuery, + data: { + count: 0, + }, + }); - client.watchQuery({ query }).subscribe({ - next: ({ data }: any) => { - if (watchCount === 0) { - expect(data.count).toEqual(0); - expect({ ...data.user }).toMatchObject({ - __typename: "User", - firstName: "John", - }); - watchCount += 1; - client.mutate({ - mutation, - update(proxy, { data: { updateUser } }) { - proxy.writeQuery({ - query: userQuery, - data: { - user: { ...updateUser }, - }, - }); - }, - }); - } else { - expect(data.count).toEqual(1); - expect({ ...data.user }).toMatchObject({ - __typename: "User", - firstName: "Harry", - }); - resolve(); - } - }, - }); - } - ); + const stream = new ObservableStream(client.watchQuery({ query })); - itAsync( - "handles server errors when root data property is null", - (resolve, reject) => { - const query = gql` - query GetUser { - user { - firstName @client - lastName - } - } - `; + await expect(stream).toEmitMatchedValue({ + data: { + count: 0, + user: { __typename: "User", firstName: "John" }, + }, + }); - const cache = new InMemoryCache(); - const link = new ApolloLink((operation) => { - return Observable.of({ - data: null, - errors: [ - new GraphQLError("something went wrong", { - extensions: { - code: "INTERNAL_SERVER_ERROR", - }, - path: ["user"], - }), - ], + await client.mutate({ + mutation, + update(proxy, { data: { updateUser } }) { + proxy.writeQuery({ + query: userQuery, + data: { + user: { ...updateUser }, + }, }); - }); + }, + }); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + await expect(stream).toEmitMatchedValue({ + data: { + count: 1, + user: { __typename: "User", firstName: "Harry" }, + }, + }); + }); - client.watchQuery({ query }).subscribe({ - error(error) { - expect(error.message).toEqual("something went wrong"); - resolve(); - }, - next() { - reject(); - }, + it("handles server errors when root data property is null", async () => { + const query = gql` + query GetUser { + user { + firstName @client + lastName + } + } + `; + + const cache = new InMemoryCache(); + const link = new ApolloLink((operation) => { + return Observable.of({ + data: null, + errors: [ + new GraphQLError("something went wrong", { + extensions: { + code: "INTERNAL_SERVER_ERROR", + }, + path: ["user"], + }), + ], }); - } - ); + }); + + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + const stream = new ObservableStream(client.watchQuery({ query })); + + await expect(stream).toEmitError("something went wrong"); + }); }); diff --git a/src/__tests__/local-state/resolvers.ts b/src/__tests__/local-state/resolvers.ts index b305a3ed7d..b1941cd80c 100644 --- a/src/__tests__/local-state/resolvers.ts +++ b/src/__tests__/local-state/resolvers.ts @@ -1,27 +1,19 @@ import gql from "graphql-tag"; import { DocumentNode, ExecutionResult } from "graphql"; -import { assign } from "lodash"; import { LocalState } from "../../core/LocalState"; -import { - ApolloClient, - ApolloQueryResult, - Resolvers, - WatchQueryOptions, -} from "../../core"; +import { ApolloClient, ApolloQueryResult, Resolvers } from "../../core"; import { InMemoryCache, isReference } from "../../cache"; -import { Observable, Observer } from "../../utilities"; +import { Observable } from "../../utilities"; import { ApolloLink } from "../../link/core"; -import { itAsync } from "../../testing"; import mockQueryManager from "../../testing/core/mocking/mockQueryManager"; -import wrap from "../../testing/core/wrap"; +import { ObservableStream } from "../../testing/internal"; // Helper method that sets up a mockQueryManager and then passes on the // results to an observer. -const assertWithObserver = ({ - reject, +const setupTestWithResolvers = ({ resolvers, query, serverQuery, @@ -30,10 +22,8 @@ const assertWithObserver = ({ serverResult, error, delay, - observer, }: { - reject: (reason: any) => any; - resolvers?: Resolvers; + resolvers: Resolvers; query: DocumentNode; serverQuery?: DocumentNode; variables?: object; @@ -41,7 +31,6 @@ const assertWithObserver = ({ error?: Error; serverResult?: ExecutionResult; delay?: number; - observer: Observer>; }) => { const queryManager = mockQueryManager({ request: { query: serverQuery || query, variables }, @@ -50,22 +39,15 @@ const assertWithObserver = ({ delay, }); - if (resolvers) { - queryManager.getLocalState().addResolvers(resolvers); - } - - const finalOptions = assign( - { query, variables }, - queryOptions - ) as WatchQueryOptions; - return queryManager.watchQuery(finalOptions).subscribe({ - next: wrap(reject, observer.next!), - error: observer.error, - }); + queryManager.getLocalState().addResolvers(resolvers); + + return new ObservableStream( + queryManager.watchQuery({ query, variables, ...queryOptions }) + ); }; describe("Basic resolver capabilities", () => { - itAsync("should run resolvers for @client queries", (resolve, reject) => { + it("should run resolvers for @client queries", async () => { const query = gql` query Test { foo @client { @@ -80,234 +62,183 @@ describe("Basic resolver capabilities", () => { }, }; - assertWithObserver({ - reject, + const stream = setupTestWithResolvers({ resolvers, query }); + + await expect(stream).toEmitMatchedValue({ data: { foo: { bar: true } } }); + }); + + it("should handle queries with a mix of @client and server fields", async () => { + const query = gql` + query Mixed { + foo @client { + bar + } + bar { + baz + } + } + `; + + const serverQuery = gql` + query Mixed { + bar { + baz + } + } + `; + + const resolvers = { + Query: { + foo: () => ({ bar: true }), + }, + }; + + const stream = setupTestWithResolvers({ resolvers, query, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: true } }); - } catch (error) { - reject(error); - } - resolve(); - }, + serverQuery, + serverResult: { data: { bar: { baz: true } } }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + foo: { bar: true }, + bar: { baz: true }, }, }); }); - itAsync( - "should handle queries with a mix of @client and server fields", - (resolve, reject) => { - const query = gql` - query Mixed { - foo @client { - bar - } - bar { - baz - } - } - `; + it("should handle a mix of @client fields with fragments and server fields", async () => { + const query = gql` + fragment client on ClientData { + bar + __typename + } - const serverQuery = gql` - query Mixed { - bar { - baz - } + query Mixed { + foo @client { + ...client } - `; - - const resolvers = { - Query: { - foo: () => ({ bar: true }), - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { data: { bar: { baz: true } } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: true }, bar: { baz: true } }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); - - itAsync( - "should handle a mix of @client fields with fragments and server fields", - (resolve, reject) => { - const query = gql` - fragment client on ClientData { - bar - __typename + bar { + baz } + } + `; - query Mixed { - foo @client { - ...client - } - bar { - baz - } + const serverQuery = gql` + query Mixed { + bar { + baz } - `; + } + `; - const serverQuery = gql` - query Mixed { - bar { - baz - } - } - `; + const resolvers = { + Query: { + foo: () => ({ bar: true, __typename: "ClientData" }), + }, + }; - const resolvers = { - Query: { - foo: () => ({ bar: true, __typename: "ClientData" }), - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { data: { bar: { baz: true, __typename: "Bar" } } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ - foo: { bar: true, __typename: "ClientData" }, - bar: { baz: true }, - }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { data: { bar: { baz: true, __typename: "Bar" } } }, + }); - itAsync( - "should handle @client fields inside fragments", - (resolve, reject) => { - const query = gql` - fragment Foo on Foo { - bar - ...Foo2 - } - fragment Foo2 on Foo { - __typename - baz @client + await expect(stream).toEmitMatchedValue({ + data: { + foo: { bar: true, __typename: "ClientData" }, + bar: { baz: true }, + }, + }); + }); + + it("should handle @client fields inside fragments", async () => { + const query = gql` + fragment Foo on Foo { + bar + ...Foo2 + } + fragment Foo2 on Foo { + __typename + baz @client + } + query Mixed { + foo { + ...Foo } - query Mixed { - foo { - ...Foo - } - bar { - baz - } + bar { + baz } - `; + } + `; - const serverQuery = gql` - fragment Foo on Foo { - bar + const serverQuery = gql` + fragment Foo on Foo { + bar + } + query Mixed { + foo { + ...Foo } - query Mixed { - foo { - ...Foo - } - bar { - baz - } + bar { + baz } - `; + } + `; - const resolvers = { - Foo: { - baz: () => false, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { - data: { foo: { bar: true, __typename: `Foo` }, bar: { baz: true } }, - }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ - foo: { bar: true, baz: false, __typename: "Foo" }, - bar: { baz: true }, - }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const resolvers = { + Foo: { + baz: () => false, + }, + }; - itAsync( - "should have access to query variables when running @client resolvers", - (resolve, reject) => { - const query = gql` - query WithVariables($id: ID!) { - foo @client { - bar(id: $id) - } + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { + data: { foo: { bar: true, __typename: `Foo` }, bar: { baz: true } }, + }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + foo: { bar: true, baz: false, __typename: "Foo" }, + bar: { baz: true }, + }, + }); + }); + + it("should have access to query variables when running @client resolvers", async () => { + const query = gql` + query WithVariables($id: ID!) { + foo @client { + bar(id: $id) } - `; + } + `; - const resolvers = { - Query: { - foo: () => ({ __typename: "Foo" }), - }, - Foo: { - bar: (_data: any, { id }: { id: number }) => id, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - variables: { id: 1 }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: 1 } }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const resolvers = { + Query: { + foo: () => ({ __typename: "Foo" }), + }, + Foo: { + bar: (_data: any, { id }: { id: number }) => id, + }, + }; + + const stream = setupTestWithResolvers({ + resolvers, + query, + variables: { id: 1 }, + }); + + await expect(stream).toEmitMatchedValue({ data: { foo: { bar: 1 } } }); + }); - itAsync("should pass context to @client resolvers", (resolve, reject) => { + it("should pass context to @client resolvers", async () => { const query = gql` query WithContext { foo @client { @@ -325,127 +256,99 @@ describe("Basic resolver capabilities", () => { }, }; - assertWithObserver({ - reject, + const stream = setupTestWithResolvers({ resolvers, query, queryOptions: { context: { id: 1 } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: 1 } }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, }); + + await expect(stream).toEmitMatchedValue({ data: { foo: { bar: 1 } } }); }); - itAsync( - "should combine local @client resolver results with server results, for " + - "the same field", - (resolve, reject) => { - const query = gql` - query author { - author { - name - stats { - totalPosts - postsToday @client - } + it("should combine local @client resolver results with server results, for the same field", async () => { + const query = gql` + query author { + author { + name + stats { + totalPosts + postsToday @client } } - `; + } + `; - const serverQuery = gql` - query author { - author { - name - stats { - totalPosts - } + const serverQuery = gql` + query author { + author { + name + stats { + totalPosts } } - `; + } + `; - const resolvers = { - Stats: { - postsToday: () => 10, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { - data: { - author: { - name: "John Smith", - stats: { - totalPosts: 100, - __typename: "Stats", - }, - __typename: "Author", + const resolvers = { + Stats: { + postsToday: () => 10, + }, + }; + + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { + data: { + author: { + name: "John Smith", + stats: { + totalPosts: 100, + __typename: "Stats", }, + __typename: "Author", }, }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ - author: { - name: "John Smith", - stats: { - totalPosts: 100, - postsToday: 10, - }, - }, - }); - } catch (error) { - reject(error); - } - resolve(); + }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + author: { + name: "John Smith", + stats: { + totalPosts: 100, + postsToday: 10, }, }, - }); - } - ); + }, + }); + }); - itAsync( - "should handle resolvers that work with booleans properly", - (resolve, reject) => { - const query = gql` - query CartDetails { - isInCart @client - } - `; + it("should handle resolvers that work with booleans properly", async () => { + const query = gql` + query CartDetails { + isInCart @client + } + `; - const cache = new InMemoryCache(); - cache.writeQuery({ query, data: { isInCart: true } }); + const cache = new InMemoryCache(); + cache.writeQuery({ query, data: { isInCart: true } }); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - isInCart: () => false, - }, + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + isInCart: () => false, }, - }); + }, + }); - return client - .query({ query, fetchPolicy: "network-only" }) - .then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - isInCart: false, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query, fetchPolicy: "network-only" }); + + expect(data).toMatchObject({ isInCart: false }); + }); it("should handle nested asynchronous @client resolvers (issue #4841)", () => { const query = gql` @@ -569,57 +472,47 @@ describe("Basic resolver capabilities", () => { ]); }); - itAsync( - "should not run resolvers without @client directive (issue #9571)", - (resolve, reject) => { - const query = gql` - query Mixed { - foo @client { - bar - } - bar { - baz - } + it("should not run resolvers without @client directive (issue #9571)", async () => { + const query = gql` + query Mixed { + foo @client { + bar + } + bar { + baz } - `; + } + `; - const serverQuery = gql` - query Mixed { - bar { - baz - } + const serverQuery = gql` + query Mixed { + bar { + baz } - `; + } + `; - const barResolver = jest.fn(() => ({ __typename: `Bar`, baz: false })); + const barResolver = jest.fn(() => ({ __typename: `Bar`, baz: false })); - const resolvers = { - Query: { - foo: () => ({ __typename: `Foo`, bar: true }), - bar: barResolver, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { data: { bar: { baz: true } } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: true }, bar: { baz: true } }); - expect(barResolver).not.toHaveBeenCalled(); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const resolvers = { + Query: { + foo: () => ({ __typename: `Foo`, bar: true }), + bar: barResolver, + }, + }; + + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { data: { bar: { baz: true } } }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { foo: { bar: true }, bar: { baz: true } }, + }); + expect(barResolver).not.toHaveBeenCalled(); + }); }); describe("Writing cache data from resolvers", () => { @@ -777,440 +670,394 @@ describe("Writing cache data from resolvers", () => { }); describe("Resolving field aliases", () => { - itAsync( - "should run resolvers for missing client queries with aliased field", - (resolve, reject) => { - // expect.assertions(1); - const query = gql` - query Aliased { - foo @client { - bar - } - baz: bar { - foo - } + it("should run resolvers for missing client queries with aliased field", async () => { + const query = gql` + query Aliased { + foo @client { + bar } - `; + baz: bar { + foo + } + } + `; - const link = new ApolloLink(() => - // Each link is responsible for implementing their own aliasing so it - // returns baz not bar - Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) - ); + const link = new ApolloLink(() => + // Each link is responsible for implementing their own aliasing so it + // returns baz not bar + Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) + ); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Query: { - foo: () => ({ bar: true, __typename: "Foo" }), - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Query: { + foo: () => ({ bar: true, __typename: "Foo" }), }, - }); + }, + }); - client.query({ query }).then(({ data }) => { - try { - expect(data).toEqual({ - foo: { bar: true, __typename: "Foo" }, - baz: { foo: true, __typename: "Baz" }, - }); - } catch (e) { - reject(e); - return; - } - resolve(); - }, reject); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should run resolvers for client queries when aliases are in use on " + - "the @client-tagged node", - (resolve, reject) => { - const aliasedQuery = gql` - query Test { - fie: foo @client { - bar - } - } - `; - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.empty(), - resolvers: { - Query: { - foo: () => ({ bar: true, __typename: "Foo" }), - fie: () => { - reject( - "Called the resolver using the alias' name, instead of " + - "the correct resolver name." - ); - }, - }, - }, - }); + expect(data).toEqual({ + foo: { bar: true, __typename: "Foo" }, + baz: { foo: true, __typename: "Baz" }, + }); + }); - client.query({ query: aliasedQuery }).then(({ data }) => { - expect(data).toEqual({ fie: { bar: true, __typename: "Foo" } }); - resolve(); - }, reject); - } - ); + it("should run resolvers for client queries when aliases are in use on the @client-tagged node", async () => { + const aliasedQuery = gql` + query Test { + fie: foo @client { + bar + } + } + `; - itAsync( - "should respect aliases for *nested fields* on the @client-tagged node", - (resolve, reject) => { - const aliasedQuery = gql` - query Test { - fie: foo @client { - fum: bar - } - baz: bar { - foo - } + const fie = jest.fn(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + resolvers: { + Query: { + foo: () => ({ bar: true, __typename: "Foo" }), + fie, + }, + }, + }); + + const { data } = await client.query({ query: aliasedQuery }); + + expect(data).toEqual({ fie: { bar: true, __typename: "Foo" } }); + expect(fie).not.toHaveBeenCalled(); + }); + + it("should respect aliases for *nested fields* on the @client-tagged node", async () => { + const aliasedQuery = gql` + query Test { + fie: foo @client { + fum: bar } - `; + baz: bar { + foo + } + } + `; - const link = new ApolloLink(() => - Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) - ); + const link = new ApolloLink(() => + Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) + ); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Query: { - foo: () => ({ bar: true, __typename: "Foo" }), - fie: () => { - reject( - "Called the resolver using the alias' name, instead of " + - "the correct resolver name." - ); - }, - }, + const fie = jest.fn(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Query: { + foo: () => ({ bar: true, __typename: "Foo" }), + fie, }, - }); + }, + }); - client.query({ query: aliasedQuery }).then(({ data }) => { - expect(data).toEqual({ - fie: { fum: true, __typename: "Foo" }, - baz: { foo: true, __typename: "Baz" }, - }); - resolve(); - }, reject); - } - ); + const { data } = await client.query({ query: aliasedQuery }); + + expect(data).toEqual({ + fie: { fum: true, __typename: "Foo" }, + baz: { foo: true, __typename: "Baz" }, + }); + expect(fie).not.toHaveBeenCalled(); + }); - it( - "should pull initialized values for aliased fields tagged with @client " + - "from the cache", - () => { - const query = gql` + it("should pull initialized values for aliased fields tagged with @client from the cache", async () => { + const query = gql` + { + fie: foo @client { + bar + } + } + `; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + resolvers: {}, + }); + + cache.writeQuery({ + query: gql` { - fie: foo @client { + foo { bar } } - `; + `, + data: { + foo: { + bar: "yo", + __typename: "Foo", + }, + }, + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link: ApolloLink.empty(), - resolvers: {}, - }); + const { data } = await client.query({ query }); - cache.writeQuery({ - query: gql` - { - foo { - bar - } - } - `, + expect({ ...data }).toMatchObject({ + fie: { bar: "yo", __typename: "Foo" }, + }); + }); + + it("should resolve @client fields using local resolvers and not have their value overridden when a fragment is loaded", async () => { + const query = gql` + fragment LaunchDetails on Launch { + id + __typename + } + query Launch { + launch { + isInCart @client + ...LaunchDetails + } + } + `; + + const link = new ApolloLink(() => + Observable.of({ data: { - foo: { - bar: "yo", - __typename: "Foo", + launch: { + id: 1, + __typename: "Launch", }, }, - }); + }) + ); - return client.query({ query }).then(({ data }) => { - expect({ ...data }).toMatchObject({ - fie: { bar: "yo", __typename: "Foo" }, - }); - }); - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Launch: { + isInCart() { + return true; + }, + }, + }, + }); - it( - "should resolve @client fields using local resolvers and not have " + - "their value overridden when a fragment is loaded", - () => { - const query = gql` - fragment LaunchDetails on Launch { - id - __typename - } - query Launch { + client.writeQuery({ + query: gql` + { launch { - isInCart @client - ...LaunchDetails + isInCart } } - `; - - const link = new ApolloLink(() => - Observable.of({ - data: { - launch: { - id: 1, - __typename: "Launch", - }, - }, - }) - ); - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Launch: { - isInCart() { - return true; - }, - }, + `, + data: { + launch: { + isInCart: false, + __typename: "Launch", }, - }); + }, + }); - client.writeQuery({ - query: gql` - { - launch { - isInCart - } - } - `, - data: { - launch: { - isInCart: false, - __typename: "Launch", - }, - }, - }); + { + const { data } = await client.query({ query }); + // `isInCart` resolver is fired, returning `true` (which is then + // stored in the cache). + expect(data.launch.isInCart).toBe(true); + } - return client - .query({ query }) - .then(({ data }) => { - // `isInCart` resolver is fired, returning `true` (which is then - // stored in the cache). - expect(data.launch.isInCart).toBe(true); - }) - .then(() => { - client.query({ query }).then(({ data }) => { - // When the same query fires again, `isInCart` should be pulled from - // the cache and have a value of `true`. - expect(data.launch.isInCart).toBe(true); - }); - }); + { + const { data } = await client.query({ query }); + // When the same query fires again, `isInCart` should be pulled from + // the cache and have a value of `true`. + expect(data.launch.isInCart).toBe(true); } - ); + }); }); describe("Force local resolvers", () => { - it( - "should force the running of local resolvers marked with " + - "`@client(always: true)` when using `ApolloClient.query`", - async () => { - const query = gql` - query Author { - author { - name - isLoggedIn @client(always: true) - } + it("should force the running of local resolvers marked with `@client(always: true)` when using `ApolloClient.query`", async () => { + const query = gql` + query Author { + author { + name + isLoggedIn @client(always: true) } - `; + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link: ApolloLink.empty(), - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + resolvers: {}, + }); + + cache.writeQuery({ + query, + data: { + author: { + name: "John Smith", + isLoggedIn: false, + __typename: "Author", + }, + }, + }); + + // When the resolver isn't defined, there isn't anything to force, so + // make sure the query resolves from the cache properly. + const { data: data1 } = await client.query({ query }); + expect(data1.author.isLoggedIn).toEqual(false); + + client.addResolvers({ + Author: { + isLoggedIn() { + return true; + }, + }, + }); + + // A resolver is defined, so make sure it's forced, and the result + // resolves properly as a combination of cache and local resolver + // data. + const { data: data2 } = await client.query({ query }); + expect(data2.author.isLoggedIn).toEqual(true); + }); + + it("should avoid running forced resolvers a second time when loading results over the network (so not from the cache)", async () => { + const query = gql` + query Author { + author { + name + isLoggedIn @client(always: true) + } + } + `; - cache.writeQuery({ - query, + const link = new ApolloLink(() => + Observable.of({ data: { author: { name: "John Smith", - isLoggedIn: false, __typename: "Author", }, }, - }); - - // When the resolver isn't defined, there isn't anything to force, so - // make sure the query resolves from the cache properly. - const { data: data1 } = await client.query({ query }); - expect(data1.author.isLoggedIn).toEqual(false); + }) + ); - client.addResolvers({ + let count = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { Author: { isLoggedIn() { + count += 1; return true; }, }, - }); + }, + }); - // A resolver is defined, so make sure it's forced, and the result - // resolves properly as a combination of cache and local resolver - // data. - const { data: data2 } = await client.query({ query }); - expect(data2.author.isLoggedIn).toEqual(true); - } - ); + const { data } = await client.query({ query }); + expect(data.author.isLoggedIn).toEqual(true); + expect(count).toEqual(1); + }); - it( - "should avoid running forced resolvers a second time when " + - "loading results over the network (so not from the cache)", - async () => { - const query = gql` - query Author { - author { - name - isLoggedIn @client(always: true) - } - } - `; - - const link = new ApolloLink(() => - Observable.of({ - data: { - author: { - name: "John Smith", - __typename: "Author", - }, - }, - }) - ); + it("should only force resolvers for fields marked with `@client(always: true)`, not all `@client` fields", async () => { + const query = gql` + query UserDetails { + name @client + isLoggedIn @client(always: true) + } + `; - let count = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Author: { - isLoggedIn() { - count += 1; - return true; - }, + let nameCount = 0; + let isLoggedInCount = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + resolvers: { + Query: { + name() { + nameCount += 1; + return "John Smith"; + }, + isLoggedIn() { + isLoggedInCount += 1; + return true; }, }, - }); + }, + }); - const { data } = await client.query({ query }); - expect(data.author.isLoggedIn).toEqual(true); - expect(count).toEqual(1); - } - ); + await client.query({ query }); + expect(nameCount).toEqual(1); + expect(isLoggedInCount).toEqual(1); - it( - "should only force resolvers for fields marked with " + - "`@client(always: true)`, not all `@client` fields", - async () => { - const query = gql` - query UserDetails { - name @client - isLoggedIn @client(always: true) - } - `; - - let nameCount = 0; - let isLoggedInCount = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - resolvers: { - Query: { - name() { - nameCount += 1; - return "John Smith"; - }, - isLoggedIn() { - isLoggedInCount += 1; - return true; - }, + // On the next request, `name` will be loaded from the cache only, + // whereas `isLoggedIn` will be loaded from the cache then overwritten + // by running its forced local resolver. + await client.query({ query }); + expect(nameCount).toEqual(1); + expect(isLoggedInCount).toEqual(2); + }); + + it("should force the running of local resolvers marked with `@client(always: true)` when using `ApolloClient.watchQuery`", async () => { + const query = gql` + query IsUserLoggedIn { + isUserLoggedIn @client(always: true) + } + `; + + const queryNoForce = gql` + query IsUserLoggedIn { + isUserLoggedIn @client + } + `; + + let callCount = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + resolvers: { + Query: { + isUserLoggedIn() { + callCount += 1; + return true; }, }, - }); + }, + }); - await client.query({ query }); - expect(nameCount).toEqual(1); - expect(isLoggedInCount).toEqual(1); + { + const stream = new ObservableStream(client.watchQuery({ query })); - // On the next request, `name` will be loaded from the cache only, - // whereas `isLoggedIn` will be loaded from the cache then overwritten - // by running its forced local resolver. - await client.query({ query }); - expect(nameCount).toEqual(1); - expect(isLoggedInCount).toEqual(2); + await expect(stream).toEmitNext(); + expect(callCount).toBe(1); } - ); - itAsync( - "should force the running of local resolvers marked with " + - "`@client(always: true)` when using `ApolloClient.watchQuery`", - (resolve, reject) => { - const query = gql` - query IsUserLoggedIn { - isUserLoggedIn @client(always: true) - } - `; - - const queryNoForce = gql` - query IsUserLoggedIn { - isUserLoggedIn @client - } - `; - - let callCount = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - resolvers: { - Query: { - isUserLoggedIn() { - callCount += 1; - return true; - }, - }, - }, - }); + { + const stream = new ObservableStream(client.watchQuery({ query })); - client.watchQuery({ query }).subscribe({ - next() { - expect(callCount).toBe(1); + await expect(stream).toEmitNext(); + expect(callCount).toBe(2); + } - client.watchQuery({ query }).subscribe({ - next() { - expect(callCount).toBe(2); + { + const stream = new ObservableStream( + client.watchQuery({ query: queryNoForce }) + ); - client.watchQuery({ query: queryNoForce }).subscribe({ - next() { - // Result is loaded from the cache since the resolver - // isn't being forced. - expect(callCount).toBe(2); - resolve(); - }, - }); - }, - }); - }, - }); + await expect(stream).toEmitNext(); + // Result is loaded from the cache since the resolver + // isn't being forced. + expect(callCount).toBe(2); } - ); + }); - it("should allow client-only virtual resolvers (#4731)", function () { + it("should allow client-only virtual resolvers (#4731)", async () => { const query = gql` query UserData { userData @client { @@ -1241,21 +1088,21 @@ describe("Force local resolvers", () => { }, }); - return client.query({ query }).then((result) => { - expect(result.data).toEqual({ - userData: { - __typename: "User", - firstName: "Ben", - lastName: "Newman", - fullName: "Ben Newman", - }, - }); + const result = await client.query({ query }); + + expect(result.data).toEqual({ + userData: { + __typename: "User", + firstName: "Ben", + lastName: "Newman", + fullName: "Ben Newman", + }, }); }); }); describe("Async resolvers", () => { - itAsync("should support async @client resolvers", async (resolve, reject) => { + it("should support async @client resolvers", async () => { const query = gql` query Member { isLoggedIn @client @@ -1276,64 +1123,61 @@ describe("Async resolvers", () => { const { data: { isLoggedIn }, } = await client.query({ query })!; + expect(isLoggedIn).toBe(true); - return resolve(); }); - itAsync( - "should support async @client resolvers mixed with remotely resolved data", - async (resolve, reject) => { - const query = gql` - query Member { - member { - name - sessionCount @client - isLoggedIn @client - } + it("should support async @client resolvers mixed with remotely resolved data", async () => { + const query = gql` + query Member { + member { + name + sessionCount @client + isLoggedIn @client } - `; - - const testMember = { - name: "John Smithsonian", - isLoggedIn: true, - sessionCount: 10, - }; - - const link = new ApolloLink(() => - Observable.of({ - data: { - member: { - name: testMember.name, - __typename: "Member", - }, + } + `; + + const testMember = { + name: "John Smithsonian", + isLoggedIn: true, + sessionCount: 10, + }; + + const link = new ApolloLink(() => + Observable.of({ + data: { + member: { + name: testMember.name, + __typename: "Member", }, - }) - ); + }, + }) + ); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Member: { - isLoggedIn() { - return Promise.resolve(testMember.isLoggedIn); - }, - sessionCount() { - return testMember.sessionCount; - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Member: { + isLoggedIn() { + return Promise.resolve(testMember.isLoggedIn); + }, + sessionCount() { + return testMember.sessionCount; }, }, - }); + }, + }); - const { - data: { member }, - } = await client.query({ query })!; - expect(member.name).toBe(testMember.name); - expect(member.isLoggedIn).toBe(testMember.isLoggedIn); - expect(member.sessionCount).toBe(testMember.sessionCount); - return resolve(); - } - ); + const { + data: { member }, + } = await client.query({ query })!; + + expect(member.name).toBe(testMember.name); + expect(member.isLoggedIn).toBe(testMember.isLoggedIn); + expect(member.sessionCount).toBe(testMember.sessionCount); + }); }); describe("LocalState helpers", () => { diff --git a/src/cache/inmemory/__tests__/fragmentMatcher.ts b/src/cache/inmemory/__tests__/fragmentMatcher.ts index 29bce194c2..6823819226 100644 --- a/src/cache/inmemory/__tests__/fragmentMatcher.ts +++ b/src/cache/inmemory/__tests__/fragmentMatcher.ts @@ -1,6 +1,5 @@ import gql from "graphql-tag"; -import { itAsync } from "../../../testing"; import { InMemoryCache } from "../inMemoryCache"; import { visit, FragmentDefinitionNode } from "graphql"; import { hasOwn } from "../helpers"; @@ -242,7 +241,7 @@ describe("policies.fragmentMatches", () => { console.warn = warn; }); - itAsync("can infer fuzzy subtypes heuristically", (resolve, reject) => { + it("can infer fuzzy subtypes heuristically", async () => { const cache = new InMemoryCache({ possibleTypes: { A: ["B", "C"], @@ -279,7 +278,7 @@ describe("policies.fragmentMatches", () => { FragmentDefinition(frag) { function check(typename: string, result: boolean) { if (result !== cache.policies.fragmentMatches(frag, typename)) { - reject( + throw new Error( `fragment ${frag.name.value} should${ result ? "" : " not" } have matched typename ${typename}` @@ -577,7 +576,5 @@ describe("policies.fragmentMatches", () => { }, }).size ).toBe("ABCDEF".length); - - resolve(); }); }); diff --git a/src/cache/inmemory/__tests__/writeToStore.ts b/src/cache/inmemory/__tests__/writeToStore.ts index 8c8bf1c5b4..f147f5d49d 100644 --- a/src/cache/inmemory/__tests__/writeToStore.ts +++ b/src/cache/inmemory/__tests__/writeToStore.ts @@ -19,7 +19,6 @@ import { cloneDeep, getMainDefinition, } from "../../../utilities"; -import { itAsync } from "../../../testing/core"; import { StoreWriter } from "../writeToStore"; import { defaultNormalizedCacheFactory, writeQueryToStore } from "./helpers"; import { InMemoryCache } from "../inMemoryCache"; @@ -1860,137 +1859,132 @@ describe("writing to the store", () => { expect(cache.extract()).toMatchSnapshot(); }); - itAsync( - "should allow a union of objects of a different type, when overwriting a generated id with a real id", - (resolve, reject) => { - const dataWithPlaceholder = { - author: { - hello: "Foo", - __typename: "Placeholder", - }, - }; - const dataWithAuthor = { - author: { - firstName: "John", - lastName: "Smith", - id: "129", - __typename: "Author", - }, - }; - const query = gql` - query { - author { - ... on Author { - firstName - lastName - id - __typename - } - ... on Placeholder { - hello - __typename - } + it("should allow a union of objects of a different type, when overwriting a generated id with a real id", async () => { + const dataWithPlaceholder = { + author: { + hello: "Foo", + __typename: "Placeholder", + }, + }; + const dataWithAuthor = { + author: { + firstName: "John", + lastName: "Smith", + id: "129", + __typename: "Author", + }, + }; + const query = gql` + query { + author { + ... on Author { + firstName + lastName + id + __typename + } + ... on Placeholder { + hello + __typename } } - `; + } + `; - let mergeCount = 0; - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - author: { - merge(existing, incoming, { isReference, readField }) { - switch (++mergeCount) { - case 1: - expect(existing).toBeUndefined(); - expect(isReference(incoming)).toBe(false); - expect(incoming).toEqual(dataWithPlaceholder.author); - break; - case 2: - expect(existing).toEqual(dataWithPlaceholder.author); - expect(isReference(incoming)).toBe(true); - expect(readField("__typename", incoming)).toBe("Author"); - break; - case 3: - expect(isReference(existing)).toBe(true); - expect(readField("__typename", existing)).toBe("Author"); - expect(incoming).toEqual(dataWithPlaceholder.author); - break; - default: - reject("unreached"); - } - return incoming; - }, + let mergeCount = 0; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + author: { + merge(existing, incoming, { isReference, readField }) { + switch (++mergeCount) { + case 1: + expect(existing).toBeUndefined(); + expect(isReference(incoming)).toBe(false); + expect(incoming).toEqual(dataWithPlaceholder.author); + break; + case 2: + expect(existing).toEqual(dataWithPlaceholder.author); + expect(isReference(incoming)).toBe(true); + expect(readField("__typename", incoming)).toBe("Author"); + break; + case 3: + expect(isReference(existing)).toBe(true); + expect(readField("__typename", existing)).toBe("Author"); + expect(incoming).toEqual(dataWithPlaceholder.author); + break; + default: + throw new Error("unreached"); + } + return incoming; }, }, }, }, - }); + }, + }); - // write the first object, without an ID, placeholder - cache.writeQuery({ - query, - data: dataWithPlaceholder, - }); + // write the first object, without an ID, placeholder + cache.writeQuery({ + query, + data: dataWithPlaceholder, + }); - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - author: { - hello: "Foo", - __typename: "Placeholder", - }, + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + author: { + hello: "Foo", + __typename: "Placeholder", }, - }); + }, + }); - // replace with another one of different type with ID - cache.writeQuery({ - query, - data: dataWithAuthor, - }); + // replace with another one of different type with ID + cache.writeQuery({ + query, + data: dataWithAuthor, + }); - expect(cache.extract()).toEqual({ - "Author:129": { - firstName: "John", - lastName: "Smith", - id: "129", - __typename: "Author", - }, - ROOT_QUERY: { - __typename: "Query", - author: makeReference("Author:129"), - }, - }); + expect(cache.extract()).toEqual({ + "Author:129": { + firstName: "John", + lastName: "Smith", + id: "129", + __typename: "Author", + }, + ROOT_QUERY: { + __typename: "Query", + author: makeReference("Author:129"), + }, + }); - // and go back to the original: - cache.writeQuery({ - query, - data: dataWithPlaceholder, - }); + // and go back to the original: + cache.writeQuery({ + query, + data: dataWithPlaceholder, + }); - // Author__129 will remain in the store, - // but will not be referenced by any of the fields, - // hence we combine, and in that very order - expect(cache.extract()).toEqual({ - "Author:129": { - firstName: "John", - lastName: "Smith", - id: "129", - __typename: "Author", - }, - ROOT_QUERY: { - __typename: "Query", - author: { - hello: "Foo", - __typename: "Placeholder", - }, + // Author__129 will remain in the store, + // but will not be referenced by any of the fields, + // hence we combine, and in that very order + expect(cache.extract()).toEqual({ + "Author:129": { + firstName: "John", + lastName: "Smith", + id: "129", + __typename: "Author", + }, + ROOT_QUERY: { + __typename: "Query", + author: { + hello: "Foo", + __typename: "Placeholder", }, - }); - - resolve(); - } - ); + }, + }); + }); it("does not swallow errors other than field errors", () => { const query = gql` @@ -2888,29 +2882,28 @@ describe("writing to the store", () => { expect(mergeCounts).toEqual({ first: 1, second: 1, third: 1, fourth: 1 }); }); - itAsync( - "should allow silencing broadcast of cache updates", - function (resolve, reject) { - const cache = new InMemoryCache({ - typePolicies: { - Counter: { - // Counter is a singleton, but we want to be able to test - // writing to it with writeFragment, so it needs to have an ID. - keyFields: [], - }, + it("should allow silencing broadcast of cache updates", async () => { + const cache = new InMemoryCache({ + typePolicies: { + Counter: { + // Counter is a singleton, but we want to be able to test + // writing to it with writeFragment, so it needs to have an ID. + keyFields: [], }, - }); + }, + }); - const query = gql` - query { - counter { - count - } + const query = gql` + query { + counter { + count } - `; + } + `; - const results: number[] = []; + const results: number[] = []; + const promise = new Promise((resolve) => { cache.watch({ query, optimistic: true, @@ -2925,101 +2918,103 @@ describe("writing to the store", () => { resolve(); }, }); + }); - let count = 0; - - cache.writeQuery({ - query, - data: { - counter: { - __typename: "Counter", - count: ++count, - }, - }, - broadcast: false, - }); + let count = 0; - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - counter: { __ref: "Counter:{}" }, - }, - "Counter:{}": { + cache.writeQuery({ + query, + data: { + counter: { __typename: "Counter", - count: 1, + count: ++count, }, - }); + }, + broadcast: false, + }); + + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + counter: { __ref: "Counter:{}" }, + }, + "Counter:{}": { + __typename: "Counter", + count: 1, + }, + }); + + expect(results).toEqual([]); + + const counterId = cache.identify({ + __typename: "Counter", + })!; + + cache.writeFragment({ + id: counterId, + fragment: gql` + fragment Count on Counter { + count + } + `, + data: { + count: ++count, + }, + broadcast: false, + }); - expect(results).toEqual([]); + const counterMeta = { + extraRootIds: ["Counter:{}"], + }; - const counterId = cache.identify({ + expect(cache.extract()).toEqual({ + __META: counterMeta, + ROOT_QUERY: { + __typename: "Query", + counter: { __ref: "Counter:{}" }, + }, + "Counter:{}": { __typename: "Counter", - })!; + count: 2, + }, + }); - cache.writeFragment({ + expect(results).toEqual([]); + + expect( + cache.evict({ id: counterId, - fragment: gql` - fragment Count on Counter { - count - } - `, - data: { - count: ++count, - }, + fieldName: "count", broadcast: false, - }); - - const counterMeta = { - extraRootIds: ["Counter:{}"], - }; - - expect(cache.extract()).toEqual({ - __META: counterMeta, - ROOT_QUERY: { - __typename: "Query", - counter: { __ref: "Counter:{}" }, - }, - "Counter:{}": { - __typename: "Counter", - count: 2, - }, - }); + }) + ).toBe(true); - expect(results).toEqual([]); + expect(cache.extract()).toEqual({ + __META: counterMeta, + ROOT_QUERY: { + __typename: "Query", + counter: { __ref: "Counter:{}" }, + }, + "Counter:{}": { + __typename: "Counter", + }, + }); - expect( - cache.evict({ - id: counterId, - fieldName: "count", - broadcast: false, - }) - ).toBe(true); + expect(results).toEqual([]); - expect(cache.extract()).toEqual({ - __META: counterMeta, - ROOT_QUERY: { - __typename: "Query", - counter: { __ref: "Counter:{}" }, - }, - "Counter:{}": { + // Only this write should trigger a broadcast. + cache.writeQuery({ + query, + data: { + counter: { __typename: "Counter", + count: 3, }, - }); - - expect(results).toEqual([]); + }, + }); - // Only this write should trigger a broadcast. - cache.writeQuery({ - query, - data: { - counter: { - __typename: "Counter", - count: 3, - }, - }, - }); - } - ); + await promise; + }); it("writeFragment should be able to infer ROOT_QUERY", () => { const cache = new InMemoryCache(); diff --git a/src/core/__tests__/QueryManager/links.ts b/src/core/__tests__/QueryManager/links.ts index 53d3b22f6b..de43efdf8b 100644 --- a/src/core/__tests__/QueryManager/links.ts +++ b/src/core/__tests__/QueryManager/links.ts @@ -10,7 +10,7 @@ import { ApolloLink } from "../../../link/core"; import { InMemoryCache } from "../../../cache/inmemory/inMemoryCache"; // mocks -import { itAsync, MockSubscriptionLink } from "../../../testing/core"; +import { MockSubscriptionLink } from "../../../testing/core"; // core import { QueryManager } from "../../QueryManager"; @@ -18,308 +18,277 @@ import { NextLink, Operation, Reference } from "../../../core"; import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; describe("Link interactions", () => { - itAsync( - "includes the cache on the context for eviction links", - (resolve, reject) => { - const query = gql` - query CachedLuke { - people_one(id: 1) { + it("includes the cache on the context for eviction links", (done) => { + const query = gql` + query CachedLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const evictionLink = (operation: Operation, forward: NextLink) => { - const { cache } = operation.getContext(); - expect(cache).toBeDefined(); - return forward(operation).map((result) => { - setTimeout(() => { - const cacheResult = cache.read({ query }); - expect(cacheResult).toEqual(initialData); - expect(cacheResult).toEqual(result.data); - if (count === 1) { - resolve(); - } - }, 10); - return result; - }); - }; - - const mockLink = new MockSubscriptionLink(); - const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - }); + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; - let count = 0; - observable.subscribe({ - next: (result) => { - count++; - }, - error: (e) => { - console.error(e); - }, + const evictionLink = (operation: Operation, forward: NextLink) => { + const { cache } = operation.getContext(); + expect(cache).toBeDefined(); + return forward(operation).map((result) => { + setTimeout(() => { + const cacheResult = cache.read({ query }); + expect(cacheResult).toEqual(initialData); + expect(cacheResult).toEqual(result.data); + if (count === 1) { + done(); + } + }, 10); + return result; }); + }; + + const mockLink = new MockSubscriptionLink(); + const link = ApolloLink.from([evictionLink, mockLink]); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + const observable = queryManager.watchQuery({ + query, + variables: {}, + }); - // fire off first result - mockLink.simulateResult({ result: { data: initialData } }); - } - ); - itAsync( - "cleans up all links on the final unsubscribe from watchQuery", - (resolve, reject) => { - const query = gql` - query WatchedLuke { - people_one(id: 1) { + let count = 0; + observable.subscribe({ + next: (result) => { + count++; + }, + error: (e) => { + console.error(e); + }, + }); + + // fire off first result + mockLink.simulateResult({ result: { data: initialData } }); + }); + + it("cleans up all links on the final unsubscribe from watchQuery", (done) => { + const query = gql` + query WatchedLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - }); + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; - let count = 0; - let four: ObservableSubscription; - // first watch - const one = observable.subscribe((result) => count++); - // second watch - const two = observable.subscribe((result) => count++); - // third watch (to be unsubscribed) - const three = observable.subscribe((result) => { - count++; - three.unsubscribe(); - // fourth watch - four = observable.subscribe((x) => count++); - }); + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); - // fire off first result - link.simulateResult({ result: { data: initialData } }); - setTimeout(() => { - one.unsubscribe(); - - link.simulateResult({ - result: { - data: { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "R2D2" }], - }, + const observable = queryManager.watchQuery({ + query, + variables: {}, + }); + + let count = 0; + let four: ObservableSubscription; + // first watch + const one = observable.subscribe((result) => count++); + // second watch + const two = observable.subscribe((result) => count++); + // third watch (to be unsubscribed) + const three = observable.subscribe((result) => { + count++; + three.unsubscribe(); + // fourth watch + four = observable.subscribe((x) => count++); + }); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + setTimeout(() => { + one.unsubscribe(); + + link.simulateResult({ + result: { + data: { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "R2D2" }], }, }, - }); - setTimeout(() => { - four.unsubscribe(); - // final unsubscribe should be called now - two.unsubscribe(); - }, 10); + }, + }); + setTimeout(() => { + four.unsubscribe(); + // final unsubscribe should be called now + two.unsubscribe(); }, 10); + }, 10); - link.onUnsubscribe(() => { - expect(count).toEqual(6); - resolve(); - }); - } - ); - itAsync( - "cleans up all links on the final unsubscribe from watchQuery [error]", - (resolve, reject) => { - const query = gql` - query WatchedLuke { - people_one(id: 1) { + link.onUnsubscribe(() => { + expect(count).toEqual(6); + done(); + }); + }); + + it("cleans up all links on the final unsubscribe from watchQuery [error]", (done) => { + const query = gql` + query WatchedLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - }); + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; - let count = 0; - let four: ObservableSubscription; - // first watch - const one = observable.subscribe((result) => count++); - // second watch - observable.subscribe({ - next: () => count++, - error: () => { - count = 0; - }, - }); - // third watch (to be unsubscribed) - const three = observable.subscribe((result) => { - count++; - three.unsubscribe(); - // fourth watch - four = observable.subscribe((x) => count++); - }); + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); - // fire off first result - link.simulateResult({ result: { data: initialData } }); - setTimeout(() => { - one.unsubscribe(); - four.unsubscribe(); + const observable = queryManager.watchQuery({ + query, + variables: {}, + }); - // final unsubscribe should be called now - // since errors clean up subscriptions - link.simulateResult({ error: new Error("dang") }); + let count = 0; + let four: ObservableSubscription; + // first watch + const one = observable.subscribe((result) => count++); + // second watch + observable.subscribe({ + next: () => count++, + error: () => { + count = 0; + }, + }); + // third watch (to be unsubscribed) + const three = observable.subscribe((result) => { + count++; + three.unsubscribe(); + // fourth watch + four = observable.subscribe((x) => count++); + }); - setTimeout(() => { - expect(count).toEqual(0); - resolve(); - }, 10); + // fire off first result + link.simulateResult({ result: { data: initialData } }); + setTimeout(() => { + one.unsubscribe(); + four.unsubscribe(); + + // final unsubscribe should be called now + // since errors clean up subscriptions + link.simulateResult({ error: new Error("dang") }); + + setTimeout(() => { + expect(count).toEqual(0); + done(); }, 10); + }, 10); - link.onUnsubscribe(() => { - expect(count).toEqual(4); - }); - } - ); - itAsync( - "includes the cache on the context for mutations", - (resolve, reject) => { - const mutation = gql` - mutation UpdateLuke { - people_one(id: 1) { + link.onUnsubscribe(() => { + expect(count).toEqual(4); + }); + }); + + it("includes the cache on the context for mutations", (done) => { + const mutation = gql` + mutation UpdateLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const evictionLink = (operation: Operation, forward: NextLink) => { - const { cache } = operation.getContext(); - expect(cache).toBeDefined(); - resolve(); - return forward(operation); - }; - - const mockLink = new MockSubscriptionLink(); - const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - queryManager.mutate({ mutation }); - - // fire off first result - mockLink.simulateResult({ result: { data: initialData } }); - } - ); - - itAsync( - "includes passed context in the context for mutations", - (resolve, reject) => { - const mutation = gql` - mutation UpdateLuke { - people_one(id: 1) { + const evictionLink = (operation: Operation, forward: NextLink) => { + const { cache } = operation.getContext(); + expect(cache).toBeDefined(); + done(); + return forward(operation); + }; + + const mockLink = new MockSubscriptionLink(); + const link = ApolloLink.from([evictionLink, mockLink]); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + void queryManager.mutate({ mutation }); + }); + + it("includes passed context in the context for mutations", (done) => { + const mutation = gql` + mutation UpdateLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; + + const evictionLink = (operation: Operation, forward: NextLink) => { + const { planet } = operation.getContext(); + expect(planet).toBe("Tatooine"); + done(); + return forward(operation); + }; + + const mockLink = new MockSubscriptionLink(); + const link = ApolloLink.from([evictionLink, mockLink]); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + void queryManager.mutate({ mutation, context: { planet: "Tatooine" } }); + }); - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const evictionLink = (operation: Operation, forward: NextLink) => { - const { planet } = operation.getContext(); - expect(planet).toBe("Tatooine"); - resolve(); - return forward(operation); - }; - - const mockLink = new MockSubscriptionLink(); - const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - queryManager.mutate({ mutation, context: { planet: "Tatooine" } }); - - // fire off first result - mockLink.simulateResult({ result: { data: initialData } }); - } - ); it("includes getCacheKey function on the context for cache resolvers", async () => { const query = gql` { diff --git a/src/core/__tests__/fetchPolicies.ts b/src/core/__tests__/fetchPolicies.ts index 0208b6982c..8042f2712b 100644 --- a/src/core/__tests__/fetchPolicies.ts +++ b/src/core/__tests__/fetchPolicies.ts @@ -4,7 +4,7 @@ import { ApolloClient, NetworkStatus } from "../../core"; import { ApolloLink } from "../../link/core"; import { InMemoryCache } from "../../cache"; import { Observable } from "../../utilities"; -import { itAsync, mockSingleLink } from "../../testing"; +import { mockSingleLink } from "../../testing"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { WatchQueryFetchPolicy, WatchQueryOptions } from "../watchQueryOptions"; import { ApolloQueryResult } from "../types"; @@ -56,7 +56,7 @@ const mutationResult = { const merged = { author: { ...result.author, firstName: "James" } }; -const createLink = (reject: (reason: any) => any) => +const createLink = () => mockSingleLink( { request: { query }, @@ -66,7 +66,7 @@ const createLink = (reject: (reason: any) => any) => request: { query }, result: { data: result }, } - ).setOnError(reject); + ); const createFailureLink = () => mockSingleLink( @@ -80,7 +80,7 @@ const createFailureLink = () => } ); -const createMutationLink = (reject: (reason: any) => any) => +const createMutationLink = () => // fetch the data mockSingleLink( { @@ -95,41 +95,35 @@ const createMutationLink = (reject: (reason: any) => any) => request: { query }, result: { data: merged }, } - ).setOnError(reject); + ); describe("network-only", () => { - itAsync( - "requests from the network even if already in cache", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + it("requests from the network even if already in cache", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query }) - .then(() => - client - .query({ fetchPolicy: "network-only", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ query }); + const actualResult = await client.query({ + fetchPolicy: "network-only", + query, + }); + + expect(actualResult.data).toEqual(result); + expect(called).toBe(4); + }); - itAsync("saves data to the cache on success", (resolve, reject) => { + it("saves data to the cache on success", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -140,22 +134,18 @@ describe("network-only", () => { }); const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), + link: inspector.concat(createLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query, fetchPolicy: "network-only" }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(2); - }) - ) - .then(resolve, reject); + await client.query({ query, fetchPolicy: "network-only" }); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + expect(called).toBe(2); }); - itAsync("does not save data to the cache on failure", (resolve, reject) => { + it("does not save data to the cache on failure", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -171,24 +161,20 @@ describe("network-only", () => { }); let didFail = false; - return client - .query({ query, fetchPolicy: "network-only" }) - .catch((e) => { - expect(e.message).toMatch("query failed"); - didFail = true; - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the first error doesn't call .map on the inspector - expect(called).toBe(3); - expect(didFail).toBe(true); - }) - ) - .then(resolve, reject); + await client.query({ query, fetchPolicy: "network-only" }).catch((e) => { + expect(e.message).toMatch("query failed"); + didFail = true; + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the first error doesn't call .map on the inspector + expect(called).toBe(3); + expect(didFail).toBe(true); }); - itAsync("updates the cache on a mutation", (resolve, reject) => { + it("updates the cache on a mutation", async () => { const inspector = new ApolloLink((operation, forward) => { return forward(operation).map((result) => { return result; @@ -196,28 +182,23 @@ describe("network-only", () => { }); const client = new ApolloClient({ - link: inspector.concat(createMutationLink(reject)), + link: inspector.concat(createMutationLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query }) - .then(() => - // XXX currently only no-cache is supported as a fetchPolicy - // this mainly serves to ensure the cache is updated correctly - client.mutate({ mutation, variables }) - ) - .then(() => { - return client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(merged); - }); - }) - .then(resolve, reject); + await client.query({ query }); + // XXX currently only no-cache is supported as a fetchPolicy + // this mainly serves to ensure the cache is updated correctly + await client.mutate({ mutation, variables }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(merged); }); }); describe("no-cache", () => { - itAsync("requests from the network when not in cache", (resolve, reject) => { + it("requests from the network when not in cache", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -228,81 +209,62 @@ describe("no-cache", () => { }); const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), + link: inspector.concat(createLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ fetchPolicy: "no-cache", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(2); - }) - .then(resolve, reject); + const actualResult = await client.query({ fetchPolicy: "no-cache", query }); + + expect(actualResult.data).toEqual(result); + expect(called).toBe(2); }); - itAsync( - "requests from the network even if already in cache", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + it("requests from the network even if already in cache", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query }) - .then(() => - client - .query({ fetchPolicy: "no-cache", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ query }); + const actualResult = await client.query({ fetchPolicy: "no-cache", query }); - itAsync( - "does not save the data to the cache on success", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + expect(actualResult.data).toEqual(result); + expect(called).toBe(4); + }); + + it("does not save the data to the cache on success", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query, fetchPolicy: "no-cache" }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the second query couldn't read anything from the cache - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ query, fetchPolicy: "no-cache" }); + const actualResult = await client.query({ query }); - itAsync("does not save data to the cache on failure", (resolve, reject) => { + expect(actualResult.data).toEqual(result); + // the second query couldn't read anything from the cache + expect(called).toBe(4); + }); + + it("does not save data to the cache on failure", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -318,24 +280,20 @@ describe("no-cache", () => { }); let didFail = false; - return client - .query({ query, fetchPolicy: "no-cache" }) - .catch((e) => { - expect(e.message).toMatch("query failed"); - didFail = true; - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the first error doesn't call .map on the inspector - expect(called).toBe(3); - expect(didFail).toBe(true); - }) - ) - .then(resolve, reject); + await client.query({ query, fetchPolicy: "no-cache" }).catch((e) => { + expect(e.message).toMatch("query failed"); + didFail = true; + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the first error doesn't call .map on the inspector + expect(called).toBe(3); + expect(didFail).toBe(true); }); - itAsync("does not update the cache on a mutation", (resolve, reject) => { + it("does not update the cache on a mutation", async () => { const inspector = new ApolloLink((operation, forward) => { return forward(operation).map((result) => { return result; @@ -343,59 +301,46 @@ describe("no-cache", () => { }); const client = new ApolloClient({ - link: inspector.concat(createMutationLink(reject)), + link: inspector.concat(createMutationLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query }) - .then(() => - client.mutate({ mutation, variables, fetchPolicy: "no-cache" }) - ) - .then(() => { - return client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); - }) - .then(resolve, reject); + await client.query({ query }); + await client.mutate({ mutation, variables, fetchPolicy: "no-cache" }); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); }); describe("when notifyOnNetworkStatusChange is set", () => { - itAsync( - "does not save the data to the cache on success", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + it("does not save the data to the cache on success", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ - query, - fetchPolicy: "no-cache", - notifyOnNetworkStatusChange: true, - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the second query couldn't read anything from the cache - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ + query, + fetchPolicy: "no-cache", + notifyOnNetworkStatusChange: true, + }); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the second query couldn't read anything from the cache + expect(called).toBe(4); + }); - itAsync("does not save data to the cache on failure", (resolve, reject) => { + it("does not save data to the cache on failure", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -411,7 +356,7 @@ describe("no-cache", () => { }); let didFail = false; - return client + await client .query({ query, fetchPolicy: "no-cache", @@ -420,16 +365,14 @@ describe("no-cache", () => { .catch((e) => { expect(e.message).toMatch("query failed"); didFail = true; - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the first error doesn't call .map on the inspector - expect(called).toBe(3); - expect(didFail).toBe(true); - }) - ) - .then(resolve, reject); + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the first error doesn't call .map on the inspector + expect(called).toBe(3); + expect(didFail).toBe(true); }); it("gives appropriate networkStatus for watched queries", async () => { @@ -543,11 +486,7 @@ describe("cache-first", () => { results.push(result); return result; }); - }).concat( - createMutationLink((error) => { - throw error; - }) - ), + }).concat(createMutationLink()), cache: new InMemoryCache(), }); diff --git a/src/link/batch-http/__tests__/batchHttpLink.ts b/src/link/batch-http/__tests__/batchHttpLink.ts index 1209b0414c..033e97a62a 100644 --- a/src/link/batch-http/__tests__/batchHttpLink.ts +++ b/src/link/batch-http/__tests__/batchHttpLink.ts @@ -10,8 +10,8 @@ import { Observer, } from "../../../utilities/observables/Observable"; import { BatchHttpLink } from "../batchHttpLink"; -import { itAsync } from "../../../testing"; import { FetchResult } from "../../core"; +import { ObservableStream } from "../../../testing/internal"; const sampleQuery = gql` query SampleQuery { @@ -29,22 +29,6 @@ const sampleMutation = gql` } `; -function makeCallback( - resolve: () => void, - reject: (error: Error) => void, - callback: (...args: TArgs) => any -) { - return function () { - try { - // @ts-expect-error - callback.apply(this, arguments); - resolve(); - } catch (error) { - reject(error as Error); - } - } as typeof callback; -} - describe("BatchHttpLink", () => { beforeAll(() => { jest.resetModules(); @@ -76,7 +60,7 @@ describe("BatchHttpLink", () => { expect(() => new BatchHttpLink()).not.toThrow(); }); - itAsync("handles batched requests", (resolve, reject) => { + it("handles batched requests", (done) => { const clientAwareness = { name: "Some Client Name", version: "1.0.1", @@ -91,45 +75,37 @@ describe("BatchHttpLink", () => { let nextCalls = 0; let completions = 0; const next = (expectedData: any) => (data: any) => { - try { - expect(data).toEqual(expectedData); - nextCalls++; - } catch (error) { - reject(error); - } + expect(data).toEqual(expectedData); + nextCalls++; }; const complete = () => { - try { - const calls = fetchMock.calls("begin:/batch"); - expect(calls.length).toBe(1); - expect(nextCalls).toBe(2); + const calls = fetchMock.calls("begin:/batch"); + expect(calls.length).toBe(1); + expect(nextCalls).toBe(2); - const options: any = fetchMock.lastOptions("begin:/batch"); - expect(options.credentials).toEqual("two"); + const options: any = fetchMock.lastOptions("begin:/batch"); + expect(options.credentials).toEqual("two"); - const { headers } = options; - expect(headers["apollographql-client-name"]).toBeDefined(); - expect(headers["apollographql-client-name"]).toEqual( - clientAwareness.name - ); - expect(headers["apollographql-client-version"]).toBeDefined(); - expect(headers["apollographql-client-version"]).toEqual( - clientAwareness.version - ); + const { headers } = options; + expect(headers["apollographql-client-name"]).toBeDefined(); + expect(headers["apollographql-client-name"]).toEqual( + clientAwareness.name + ); + expect(headers["apollographql-client-version"]).toBeDefined(); + expect(headers["apollographql-client-version"]).toEqual( + clientAwareness.version + ); - completions++; + completions++; - if (completions === 2) { - resolve(); - } - } catch (error) { - reject(error); + if (completions === 2) { + done(); } }; const error = (error: any) => { - reject(error); + throw error; }; execute(link, { @@ -146,37 +122,34 @@ describe("BatchHttpLink", () => { }).subscribe(next(data2), error, complete); }); - itAsync( - "errors on an incorrect number of results for a batch", - (resolve, reject) => { - const link = new BatchHttpLink({ - uri: "/batch", - batchInterval: 0, - batchMax: 3, - }); + it("errors on an incorrect number of results for a batch", (done) => { + const link = new BatchHttpLink({ + uri: "/batch", + batchInterval: 0, + batchMax: 3, + }); - let errors = 0; - const next = (data: any) => { - reject("next should not have been called"); - }; + let errors = 0; + const next = (data: any) => { + throw new Error("next should not have been called"); + }; - const complete = () => { - reject("complete should not have been called"); - }; + const complete = () => { + throw new Error("complete should not have been called"); + }; - const error = (error: any) => { - errors++; + const error = (error: any) => { + errors++; - if (errors === 3) { - resolve(); - } - }; + if (errors === 3) { + done(); + } + }; - execute(link, { query: sampleQuery }).subscribe(next, error, complete); - execute(link, { query: sampleQuery }).subscribe(next, error, complete); - execute(link, { query: sampleQuery }).subscribe(next, error, complete); - } - ); + execute(link, { query: sampleQuery }).subscribe(next, error, complete); + execute(link, { query: sampleQuery }).subscribe(next, error, complete); + execute(link, { query: sampleQuery }).subscribe(next, error, complete); + }); describe("batchKey", () => { const query = gql` @@ -188,71 +161,64 @@ describe("BatchHttpLink", () => { } `; - itAsync( - "should batch queries with different options separately", - (resolve, reject) => { - let key = true; - const batchKey = () => { - key = !key; - return "" + !key; - }; + it("should batch queries with different options separately", (done) => { + let key = true; + const batchKey = () => { + key = !key; + return "" + !key; + }; - const link = ApolloLink.from([ - new BatchHttpLink({ - uri: (operation) => { - return operation.variables.endpoint; - }, - batchInterval: 1, - //if batchKey does not work, then the batch size would be 3 - batchMax: 2, - batchKey, - }), - ]); - - let count = 0; - const next = (expected: any) => (received: any) => { - try { - expect(received).toEqual(expected); - } catch (e) { - reject(e); - } - }; - const complete = () => { - count++; - if (count === 4) { - try { - const lawlCalls = fetchMock.calls("begin:/lawl"); - expect(lawlCalls.length).toBe(1); - const roflCalls = fetchMock.calls("begin:/rofl"); - expect(roflCalls.length).toBe(1); - resolve(); - } catch (e) { - reject(e); - } - } - }; + const link = ApolloLink.from([ + new BatchHttpLink({ + uri: (operation) => { + return operation.variables.endpoint; + }, + batchInterval: 1, + //if batchKey does not work, then the batch size would be 3 + batchMax: 2, + batchKey, + }), + ]); - [1, 2].forEach((x) => { - execute(link, { - query, - variables: { endpoint: "/rofl" }, - }).subscribe({ - next: next(roflData), - error: reject, - complete, - }); + let count = 0; + const next = (expected: any) => (received: any) => { + expect(received).toEqual(expected); + }; + const complete = () => { + count++; + if (count === 4) { + const lawlCalls = fetchMock.calls("begin:/lawl"); + expect(lawlCalls.length).toBe(1); + const roflCalls = fetchMock.calls("begin:/rofl"); + expect(roflCalls.length).toBe(1); + done(); + } + }; - execute(link, { - query, - variables: { endpoint: "/lawl" }, - }).subscribe({ - next: next(lawlData), - error: reject, - complete, - }); + [1, 2].forEach((x) => { + execute(link, { + query, + variables: { endpoint: "/rofl" }, + }).subscribe({ + next: next(roflData), + error: (error) => { + throw error; + }, + complete, }); - } - ); + + execute(link, { + query, + variables: { endpoint: "/lawl" }, + }).subscribe({ + next: next(lawlData), + error: (error) => { + throw error; + }, + complete, + }); + }); + }); }); }); @@ -333,127 +299,101 @@ describe("SharedHttpTest", () => { expect(() => createHttpLink()).not.toThrow(); }); - itAsync("calls next and then complete", (resolve, reject) => { - const next = jest.fn(); + it("calls next and then complete", async () => { const link = createHttpLink({ uri: "/data" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: makeCallback(resolve, reject, () => { - expect(next).toHaveBeenCalledTimes(1); - }), - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "/error" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: any) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "/error" }); const observable = execute(link, { query: sampleMutation, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: any) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync( - "strips unused variables, respecting nested fragments", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - - const query = gql` - query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { - people(surprise: $undeclared, noSurprise: $declaredAndUsed) { - ... on Doctor { - specialty(var: $usedByInlineFragment) - } - ...LawyerFragment + it("strips unused variables, respecting nested fragments", async () => { + const link = createHttpLink({ uri: "/data" }); + + const query = gql` + query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { + people(surprise: $undeclared, noSurprise: $declaredAndUsed) { + ... on Doctor { + specialty(var: $usedByInlineFragment) } + ...LawyerFragment } - fragment LawyerFragment on Lawyer { - caseCount(var: $usedByNamedFragment) - } - `; - - const variables = { - unused: "strip", - declaredButUnused: "strip", - declaredAndUsed: "keep", - undeclared: "keep", - usedByInlineFragment: "keep", - usedByNamedFragment: "keep", - }; + } + fragment LawyerFragment on Lawyer { + caseCount(var: $usedByNamedFragment) + } + `; - execute(link, { - query, - variables, - }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(JSON.parse(body as string)).toEqual([ - { - operationName: "PEOPLE", - query: print(query), - variables: { - declaredAndUsed: "keep", - undeclared: "keep", - usedByInlineFragment: "keep", - usedByNamedFragment: "keep", - }, - }, - ]); - expect(method).toBe("POST"); - expect(uri).toBe("/data"); - }), - error: (error) => reject(error), - }); - } - ); + const variables = { + unused: "strip", + declaredButUnused: "strip", + declaredAndUsed: "keep", + undeclared: "keep", + usedByInlineFragment: "keep", + usedByNamedFragment: "keep", + }; + + const stream = new ObservableStream(execute(link, { query, variables })); + + await expect(stream).toEmitNext(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + expect(JSON.parse(body as string)).toEqual([ + { + operationName: "PEOPLE", + query: print(query), + variables: { + declaredAndUsed: "keep", + undeclared: "keep", + usedByInlineFragment: "keep", + usedByNamedFragment: "keep", + }, + }, + ]); + expect(method).toBe("POST"); + expect(uri).toBe("/data"); + }); - itAsync("unsubscribes without calling subscriber", (resolve, reject) => { + it("unsubscribes without calling subscriber", async () => { const link = createHttpLink({ uri: "/data" }); const observable = execute(link, { query: sampleQuery, }); - const subscription = observable.subscribe( - (result) => reject("next should not have been called"), - (error) => reject(error), - () => reject("complete should not have been called") - ); - subscription.unsubscribe(); - expect(subscription.closed).toBe(true); - setTimeout(resolve, 50); + const stream = new ObservableStream(observable); + stream.unsubscribe(); + + await expect(stream).not.toEmitAnything(); }); - const verifyRequest = ( + const verifyRequest = async ( link: ApolloLink, - after: () => void, includeExtensions: boolean, - includeUnusedVariables: boolean, - reject: (e: Error) => void + includeUnusedVariables: boolean ) => { - const next = jest.fn(); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -462,61 +402,37 @@ describe("SharedHttpTest", () => { context, variables, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: () => { - try { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); - expect(body.query).toBe(print(sampleMutation)); - expect(body.variables).toEqual( - includeUnusedVariables ? variables : {} - ); - expect(body.context).not.toBeDefined(); - if (includeExtensions) { - expect(body.extensions).toBeDefined(); - } else { - expect(body.extensions).not.toBeDefined(); - } - expect(next).toHaveBeenCalledTimes(1); - - after(); - } catch (e) { - reject(e as Error); - } - }, - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + expect(body.query).toBe(print(sampleMutation)); + expect(body.variables).toEqual(includeUnusedVariables ? variables : {}); + expect(body.context).not.toBeDefined(); + if (includeExtensions) { + expect(body.extensions).toBeDefined(); + } else { + expect(body.extensions).not.toBeDefined(); + } }; - itAsync( - "passes all arguments to multiple fetch body including extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data", includeExtensions: true }); - verifyRequest( - link, - () => verifyRequest(link, resolve, true, false, reject), - true, - false, - reject - ); - } - ); + it("passes all arguments to multiple fetch body including extensions", async () => { + const link = createHttpLink({ uri: "/data", includeExtensions: true }); - itAsync( - "passes all arguments to multiple fetch body excluding extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - verifyRequest( - link, - () => verifyRequest(link, resolve, false, false, reject), - false, - false, - reject - ); - } - ); + await verifyRequest(link, true, false); + await verifyRequest(link, true, false); + }); + + it("passes all arguments to multiple fetch body excluding extensions", async () => { + const link = createHttpLink({ uri: "/data" }); + + await verifyRequest(link, false, false); + await verifyRequest(link, false, false); + }); - itAsync("calls multiple subscribers", (resolve, reject) => { + it("calls multiple subscribers", (done) => { const link = createHttpLink({ uri: "/data" }); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -536,57 +452,52 @@ describe("SharedHttpTest", () => { // only one call because batchHttpLink can handle more than one subscriber // without starting a new request expect(fetchMock.calls().length).toBe(1); - resolve(); + done(); }, 50); }); - itAsync( - "calls remaining subscribers after unsubscribe", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - const context = { info: "stub" }; - const variables = { params: "stub" }; + it("calls remaining subscribers after unsubscribe", (done) => { + const link = createHttpLink({ uri: "/data" }); + const context = { info: "stub" }; + const variables = { params: "stub" }; - const observable = execute(link, { - query: sampleMutation, - context, - variables, - }); + const observable = execute(link, { + query: sampleMutation, + context, + variables, + }); - observable.subscribe(subscriber); + observable.subscribe(subscriber); - setTimeout(() => { - const subscription = observable.subscribe(subscriber); - subscription.unsubscribe(); - }, 10); + setTimeout(() => { + const subscription = observable.subscribe(subscriber); + subscription.unsubscribe(); + }, 10); - setTimeout( - makeCallback(resolve, reject, () => { - expect(subscriber.next).toHaveBeenCalledTimes(1); - expect(subscriber.complete).toHaveBeenCalledTimes(1); - expect(subscriber.error).not.toHaveBeenCalled(); - resolve(); - }), - 50 - ); - } - ); + setTimeout(() => { + expect(subscriber.next).toHaveBeenCalledTimes(1); + expect(subscriber.complete).toHaveBeenCalledTimes(1); + expect(subscriber.error).not.toHaveBeenCalled(); + done(); + }, 50); + }); - itAsync("allows for dynamic endpoint setting", (resolve, reject) => { + it("allows for dynamic endpoint setting", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data" }); - execute(link, { - query: sampleQuery, - variables, - context: { uri: "/data2" }, - }).subscribe((result) => { - expect(result).toEqual(data2); - resolve(); - }); + const stream = new ObservableStream( + execute(link, { + query: sampleQuery, + variables, + context: { uri: "/data2" }, + }) + ); + + await expect(stream).toEmitValue(data2); }); - itAsync("adds headers to the request from the context", (resolve, reject) => { + it("adds headers to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -594,43 +505,42 @@ describe("SharedHttpTest", () => { }); return forward(operation).map((result) => { const { headers } = operation.getContext(); - try { - expect(headers).toBeDefined(); - } catch (e) { - reject(e); - } + expect(headers).toBeDefined(); return result; }); }); const link = middleware.concat(createHttpLink({ uri: "/data" })); - - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); }); - itAsync("adds headers to the request from the setup", (resolve, reject) => { + it("adds headers to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data", headers: { authorization: "1234" }, }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); }); it("uses the latest window.fetch function if options.fetch not configured", (done) => { @@ -688,138 +598,133 @@ describe("SharedHttpTest", () => { ); }); - itAsync( - "prioritizes context headers over setup headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { authorization: "1234" }, - }); - return forward(operation); + it("prioritizes context headers over setup headers", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + headers: { authorization: "1234" }, }); - const link = middleware.concat( - createHttpLink({ uri: "/data", headers: { authorization: "no user" } }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "/data", headers: { authorization: "no user" } }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); - itAsync( - "adds headers to the request from the context on an operation", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ uri: "/data" }); + await expect(stream).toEmitNext(); - const context = { - headers: { authorization: "1234" }, - }; + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds headers to the request from the context on an operation", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ uri: "/data" }); + + const context = { + headers: { authorization: "1234" }, + }; + const stream = new ObservableStream( execute(link, { query: sampleQuery, variables, context, - }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + }) + ); - itAsync( - "adds headers w/ preserved case to the request from the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - headers: { - authorization: "1234", - AUTHORIZATION: "1234", - "CONTENT-TYPE": "application/json", - }, - preserveHeaderCase: true, - }); + await expect(stream).toEmitNext(); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: any = fetchMock.lastCall()![1]!.headers; - expect(headers.AUTHORIZATION).toBe("1234"); - expect(headers["CONTENT-TYPE"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); - - itAsync( - "prioritizes context headers w/ preserved case over setup headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { AUTHORIZATION: "1234" }, - http: { preserveHeaderCase: true }, - }); - return forward(operation); - }); - const link = middleware.concat( - createHttpLink({ - uri: "/data", - headers: { authorization: "no user" }, - preserveHeaderCase: false, - }) - ); + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: any = fetchMock.lastCall()![1]!.headers; - expect(headers.AUTHORIZATION).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + it("adds headers w/ preserved case to the request from the setup", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + headers: { + authorization: "1234", + AUTHORIZATION: "1234", + "CONTENT-TYPE": "application/json", + }, + preserveHeaderCase: true, + }); - itAsync( - "adds headers w/ preserved case to the request from the context on an operation", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ uri: "/data" }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); - const context = { + const headers: any = fetchMock.lastCall()![1]!.headers; + expect(headers.AUTHORIZATION).toBe("1234"); + expect(headers["CONTENT-TYPE"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("prioritizes context headers w/ preserved case over setup headers", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ headers: { AUTHORIZATION: "1234" }, http: { preserveHeaderCase: true }, - }; + }); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ + uri: "/data", + headers: { authorization: "no user" }, + preserveHeaderCase: false, + }) + ); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); + + const headers: any = fetchMock.lastCall()![1]!.headers; + expect(headers.AUTHORIZATION).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds headers w/ preserved case to the request from the context on an operation", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ uri: "/data" }); + + const context = { + headers: { AUTHORIZATION: "1234" }, + http: { preserveHeaderCase: true }, + }; + const stream = new ObservableStream( execute(link, { query: sampleQuery, variables, context, - }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: any = fetchMock.lastCall()![1]!.headers; - expect(headers.AUTHORIZATION).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + }) + ); + + await expect(stream).toEmitNext(); - itAsync("adds creds to the request from the context", (resolve, reject) => { + const headers: any = fetchMock.lastCall()![1]!.headers; + expect(headers.AUTHORIZATION).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds creds to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -829,50 +734,53 @@ describe("SharedHttpTest", () => { }); const link = middleware.concat(createHttpLink({ uri: "/data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync("adds creds to the request from the setup", (resolve, reject) => { + it("adds creds to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data", credentials: "same-team-yo" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync( - "prioritizes creds from the context over the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - credentials: "same-team-yo", - }); - return forward(operation); + it("prioritizes creds from the context over the setup", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + credentials: "same-team-yo", }); - const link = middleware.concat( - createHttpLink({ uri: "/data", credentials: "error" }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "/data", credentials: "error" }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); + }); - itAsync("adds uri to the request from the context", (resolve, reject) => { + it("adds uri to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -882,27 +790,31 @@ describe("SharedHttpTest", () => { }); const link = middleware.concat(createHttpLink()); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("adds uri to the request from the setup", (resolve, reject) => { + it("adds uri to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("prioritizes context uri over setup uri", (resolve, reject) => { + it("prioritizes context uri over setup uri", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -914,82 +826,77 @@ describe("SharedHttpTest", () => { createHttpLink({ uri: "/data", credentials: "error" }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const uri = fetchMock.lastUrl(); - - expect(uri).toBe("/apollo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/apollo"); }); - itAsync("allows uri to be a function", (resolve, reject) => { + it("allows uri to be a function", async () => { const variables = { params: "stub" }; const customFetch = (_uri: any, options: any) => { const { operationName } = convertBatchedBody(options.body); - try { - expect(operationName).toBe("SampleQuery"); - } catch (e) { - reject(e); - } + expect(operationName).toBe("SampleQuery"); return fetch("/dataFunc", options); }; const link = createHttpLink({ fetch: customFetch }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - expect(fetchMock.lastUrl()).toBe("/dataFunc"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + expect(fetchMock.lastUrl()).toBe("/dataFunc"); }); - itAsync( - "adds fetchOptions to the request from the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - fetchOptions: { someOption: "foo", mode: "no-cors" }, - }); + it("adds fetchOptions to the request from the setup", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + fetchOptions: { someOption: "foo", mode: "no-cors" }, + }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const { someOption, mode, headers } = - fetchMock.lastCall()![1]! as any; - expect(someOption).toBe("foo"); - expect(mode).toBe("no-cors"); - expect(headers["content-type"]).toBe("application/json"); - }) - ); - } - ); - - itAsync( - "adds fetchOptions to the request from the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - fetchOptions: { - someOption: "foo", - }, - }); - return forward(operation); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); + + const { someOption, mode, headers } = fetchMock.lastCall()![1]! as any; + expect(someOption).toBe("foo"); + expect(mode).toBe("no-cors"); + expect(headers["content-type"]).toBe("application/json"); + }); + + it("adds fetchOptions to the request from the context", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + fetchOptions: { + someOption: "foo", + }, }); - const link = middleware.concat(createHttpLink({ uri: "/data" })); + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "/data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const { someOption } = fetchMock.lastCall()![1]! as any; - expect(someOption).toBe("foo"); - resolve(); - }) - ); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); - itAsync("uses the print option function when defined", (resolve, reject) => { + await expect(stream).toEmitNext(); + + const { someOption } = fetchMock.lastCall()![1]! as any; + expect(someOption).toBe("foo"); + }); + + it("uses the print option function when defined", async () => { const customPrinter = jest.fn( (ast: ASTNode, originalPrint: typeof print) => { return stripIgnoredCharacters(originalPrint(ast)); @@ -998,16 +905,16 @@ describe("SharedHttpTest", () => { const httpLink = createHttpLink({ uri: "data", print: customPrinter }); - execute(httpLink, { - query: sampleQuery, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(customPrinter).toHaveBeenCalledTimes(1); - }) + const stream = new ObservableStream( + execute(httpLink, { query: sampleQuery }) ); + + await expect(stream).toEmitNext(); + + expect(customPrinter).toHaveBeenCalledTimes(1); }); - itAsync("prioritizes context over setup", (resolve, reject) => { + it("prioritizes context over setup", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -1021,53 +928,53 @@ describe("SharedHttpTest", () => { createHttpLink({ uri: "/data", fetchOptions: { someOption: "bar" } }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const { someOption } = fetchMock.lastCall()![1]! as any; - expect(someOption).toBe("foo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const { someOption } = fetchMock.lastCall()![1]! as any; + expect(someOption).toBe("foo"); }); - itAsync( - "allows for not sending the query with the request", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - http: { - includeQuery: false, - includeExtensions: true, - }, - }); - operation.extensions.persistedQuery = { hash: "1234" }; - return forward(operation); + it("allows for not sending the query with the request", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + http: { + includeQuery: false, + includeExtensions: true, + }, }); - const link = middleware.concat(createHttpLink({ uri: "/data" })); + operation.extensions.persistedQuery = { hash: "1234" }; + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "/data" })); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + await expect(stream).toEmitNext(); - expect(body.query).not.toBeDefined(); - expect(body.extensions).toEqual({ persistedQuery: { hash: "1234" } }); - resolve(); - }) - ); - } - ); + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + + expect(body.query).not.toBeDefined(); + expect(body.extensions).toEqual({ persistedQuery: { hash: "1234" } }); + }); - itAsync("sets the raw response on context", (resolve, reject) => { + it("sets the raw response on context", async () => { const middleware = new ApolloLink((operation, forward) => { return new Observable((ob) => { const op = forward(operation); const sub = op.subscribe({ next: ob.next.bind(ob), error: ob.error.bind(ob), - complete: makeCallback(resolve, reject, () => { + complete: () => { expect(operation.getContext().response.headers.toBeDefined); ob.complete(); - }), + }, }); return () => { @@ -1078,12 +985,10 @@ describe("SharedHttpTest", () => { const link = middleware.concat(createHttpLink({ uri: "/data", fetch })); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - resolve(); - }, - () => {} - ); + const stream = new ObservableStream(execute(link, { query: sampleQuery })); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); it("removes @client fields from the query before sending it to the server", async () => { diff --git a/src/link/batch/__tests__/batchLink.ts b/src/link/batch/__tests__/batchLink.ts index e5930924c2..ff5aa72edd 100644 --- a/src/link/batch/__tests__/batchLink.ts +++ b/src/link/batch/__tests__/batchLink.ts @@ -4,13 +4,14 @@ import { print } from "graphql"; import { ApolloLink, execute } from "../../core"; import { Operation, FetchResult, GraphQLRequest } from "../../core/types"; import { Observable } from "../../../utilities"; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; import { BatchLink, OperationBatcher, BatchHandler, BatchableRequest, } from "../batchLink"; +import { ObservableStream } from "../../../testing/internal"; interface MockedResponse { request: GraphQLRequest; @@ -57,22 +58,6 @@ function createOperation(starting: any, operation: GraphQLRequest): Operation { return operation as Operation; } -function terminatingCheck( - resolve: () => any, - reject: (e: any) => any, - callback: (...args: TArgs) => any -) { - return function () { - try { - // @ts-expect-error - callback.apply(this, arguments); - resolve(); - } catch (error) { - reject(error); - } - } as typeof callback; -} - function requestToKey(request: GraphQLRequest): string { const queryString = typeof request.query === "string" ? request.query : print(request.query); @@ -221,195 +206,129 @@ describe("OperationBatcher", () => { } ); - itAsync( - "should be able to consume from a queue containing a single query", - (resolve, reject) => { - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchHandler, - }); + it("should be able to consume from a queue containing a single query", async () => { + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchHandler, + }); + + const observable = myBatcher.enqueueRequest({ operation }); + const stream = new ObservableStream(observable); + + const observables: (Observable | undefined)[] = + myBatcher.consumeQueue()!; - myBatcher.enqueueRequest({ operation }).subscribe( - terminatingCheck(resolve, reject, (resultObj: any) => { - expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); - expect(resultObj).toEqual({ data }); - }) - ); - const observables: (Observable | undefined)[] = - myBatcher.consumeQueue()!; - - try { - expect(observables.length).toBe(1); - } catch (e) { - reject(e); + expect(observables.length).toBe(1); + expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); + + await expect(stream).toEmitValue({ data }); + }); + + it("should be able to consume from a queue containing multiple queries", async () => { + const request2: Operation = createOperation( + {}, + { + query, } - } - ); + ); - itAsync( - "should be able to consume from a queue containing multiple queries", - (resolve, reject) => { - const request2: Operation = createOperation( - {}, - { - query, - } - ); + const BH = createMockBatchHandler( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data }, + } + ); - const BH = createMockBatchHandler( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data }, - } - ); + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchMax: 10, + batchHandler: BH, + }); + const observable1 = myBatcher.enqueueRequest({ operation }); + const observable2 = myBatcher.enqueueRequest({ operation: request2 }); - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchMax: 10, - batchHandler: BH, - }); - const observable1 = myBatcher.enqueueRequest({ operation }); - const observable2 = myBatcher.enqueueRequest({ operation: request2 }); - let notify = false; - observable1.subscribe((resultObj1) => { - try { - expect(resultObj1).toEqual({ data }); - } catch (e) { - reject(e); - } + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + expect(myBatcher["batchesByKey"].get("")!.size).toBe(2); + const observables: (Observable | undefined)[] = + myBatcher.consumeQueue()!; + expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); + expect(observables.length).toBe(2); - observable2.subscribe((resultObj2) => { - try { - expect(resultObj2).toEqual({ data }); - } catch (e) { - reject(e); - } + await expect(stream1).toEmitValue({ data }); + await expect(stream2).toEmitValue({ data }); + }); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + it("should be able to consume from a queue containing multiple queries with different batch keys", async () => { + // NOTE: this test was added to ensure that queries don't "hang" when consumed by BatchLink. + // "Hanging" in this case results in this test never resolving. So + // if this test times out it's probably a real issue and not a flake + const request2: Operation = createOperation( + {}, + { + query, + } + ); - try { - expect(myBatcher["batchesByKey"].get("")!.size).toBe(2); - const observables: (Observable | undefined)[] = - myBatcher.consumeQueue()!; - expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); - expect(observables.length).toBe(2); - } catch (e) { - reject(e); + const BH = createMockBatchHandler( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data }, } - } - ); + ); - itAsync( - "should be able to consume from a queue containing multiple queries with different batch keys", - (resolve, reject) => { - // NOTE: this test was added to ensure that queries don't "hang" when consumed by BatchLink. - // "Hanging" in this case results in this test never resolving. So - // if this test times out it's probably a real issue and not a flake - const request2: Operation = createOperation( - {}, - { - query, - } - ); + let key = true; + const batchKey = () => { + key = !key; + return "" + !key; + }; - const BH = createMockBatchHandler( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data }, - } - ); - - let key = true; - const batchKey = () => { - key = !key; - return "" + !key; - }; - - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchMax: 10, - batchHandler: BH, - batchKey, - }); + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchMax: 10, + batchHandler: BH, + batchKey, + }); - const observable1 = myBatcher.enqueueRequest({ operation }); - const observable2 = myBatcher.enqueueRequest({ operation: request2 }); + const observable1 = myBatcher.enqueueRequest({ operation }); + const observable2 = myBatcher.enqueueRequest({ operation: request2 }); - let notify = false; - observable1.subscribe((resultObj1) => { - try { - expect(resultObj1).toEqual({ data }); - } catch (e) { - reject(e); - } + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + jest.runAllTimers(); - observable2.subscribe((resultObj2) => { - try { - expect(resultObj2).toEqual({ data }); - } catch (e) { - reject(e); - } + await expect(stream1).toEmitValue({ data }); + await expect(stream2).toEmitValue({ data }); + }); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + it("should return a promise when we enqueue a request and resolve it with a result", async () => { + const BH = createMockBatchHandler({ + request: { query }, + result: { data }, + }); + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: BH, + }); + const observable = myBatcher.enqueueRequest({ operation }); + const stream = new ObservableStream(observable); - jest.runAllTimers(); - } - ); + myBatcher.consumeQueue(); - itAsync( - "should return a promise when we enqueue a request and resolve it with a result", - (resolve, reject) => { - const BH = createMockBatchHandler({ - request: { query }, - result: { data }, - }); - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: BH, - }); - const observable = myBatcher.enqueueRequest({ operation }); - observable.subscribe( - terminatingCheck(resolve, reject, (result: any) => { - expect(result).toEqual({ data }); - }) - ); - myBatcher.consumeQueue(); - } - ); + await expect(stream).toEmitValue({ data }); + }); - itAsync("should be able to debounce requests", (resolve, reject) => { + it("should be able to debounce requests", () => { const batchInterval = 10; const myBatcher = new OperationBatcher({ batchDebounce: true, @@ -442,11 +361,10 @@ describe("OperationBatcher", () => { // and expect the queue to be empty. jest.advanceTimersByTime(batchInterval / 2); expect(myBatcher["batchesByKey"].size).toEqual(0); - resolve(); }); }); - itAsync("should work when single query", (resolve, reject) => { + it("should work when single query", async () => { const data = { lastName: "Ever", firstName: "Greatest", @@ -470,152 +388,138 @@ describe("OperationBatcher", () => { const operation: Operation = createOperation({}, { query }); batcher.enqueueRequest({ operation }).subscribe({}); - try { - expect(batcher["batchesByKey"].get("")!.size).toBe(1); - } catch (e) { - reject(e); - } - - setTimeout( - terminatingCheck(resolve, reject, () => { - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - }), - 20 - ); + expect(batcher["batchesByKey"].get("")!.size).toBe(1); + const promise = wait(20); jest.runAllTimers(); + await promise; + + expect(batcher["batchesByKey"].get("")).toBeUndefined(); }); - itAsync( - "should cancel single query in queue when unsubscribing", - (resolve, reject) => { - const data = { - lastName: "Ever", - firstName: "Greatest", - }; + it("should cancel single query in queue when unsubscribing", async () => { + const data = { + lastName: "Ever", + firstName: "Greatest", + }; - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable((observer) => { - observer.next([{ data }]); - setTimeout(observer.complete.bind(observer)); - }), - }); + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable((observer) => { + observer.next([{ data }]); + setTimeout(observer.complete.bind(observer)); + }), + }); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - batcher - .enqueueRequest({ - operation: createOperation({}, { query }), - }) - .subscribe(() => reject("next should never be called")) - .unsubscribe(); + batcher + .enqueueRequest({ + operation: createOperation({}, { query }), + }) + .subscribe(() => { + throw new Error("next should never be called"); + }) + .unsubscribe(); - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - resolve(); - } - ); - - itAsync( - "should cancel single query in queue with multiple subscriptions", - (resolve, reject) => { - const data = { - lastName: "Ever", - firstName: "Greatest", - }; - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable((observer) => { - observer.next([{ data }]); - setTimeout(observer.complete.bind(observer)); - }), - }); - const query = gql` - query { - author { - firstName - lastName - } + expect(batcher["batchesByKey"].get("")).toBeUndefined(); + }); + + it("should cancel single query in queue with multiple subscriptions", () => { + const data = { + lastName: "Ever", + firstName: "Greatest", + }; + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable((observer) => { + observer.next([{ data }]); + setTimeout(observer.complete.bind(observer)); + }), + }); + const query = gql` + query { + author { + firstName + lastName } - `; - const operation: Operation = createOperation({}, { query }); + } + `; + const operation: Operation = createOperation({}, { query }); - const observable = batcher.enqueueRequest({ operation }); + const observable = batcher.enqueueRequest({ operation }); - const checkQueuedRequests = (expectedSubscriberCount: number) => { - const batch = batcher["batchesByKey"].get(""); - expect(batch).not.toBeUndefined(); - expect(batch!.size).toBe(1); - batch!.forEach((request) => { - expect(request.subscribers.size).toBe(expectedSubscriberCount); - }); - }; + const checkQueuedRequests = (expectedSubscriberCount: number) => { + const batch = batcher["batchesByKey"].get(""); + expect(batch).not.toBeUndefined(); + expect(batch!.size).toBe(1); + batch!.forEach((request) => { + expect(request.subscribers.size).toBe(expectedSubscriberCount); + }); + }; - const sub1 = observable.subscribe(() => - reject("next should never be called") - ); - checkQueuedRequests(1); + const sub1 = observable.subscribe(() => { + throw new Error("next should never be called"); + }); + checkQueuedRequests(1); - const sub2 = observable.subscribe(() => - reject("next should never be called") - ); - checkQueuedRequests(2); + const sub2 = observable.subscribe(() => { + throw new Error("next should never be called"); + }); + checkQueuedRequests(2); - sub1.unsubscribe(); - checkQueuedRequests(1); + sub1.unsubscribe(); + checkQueuedRequests(1); - sub2.unsubscribe(); - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - resolve(); - } - ); + sub2.unsubscribe(); + expect(batcher["batchesByKey"].get("")).toBeUndefined(); + }); - itAsync( - "should cancel single query in flight when unsubscribing", - (resolve, reject) => { - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable(() => { - // Instead of typically starting an XHR, we trigger the unsubscription from outside - setTimeout(() => subscription?.unsubscribe(), 5); - - return () => { - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - resolve(); - }; - }), - }); + it("should cancel single query in flight when unsubscribing", (done) => { + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable(() => { + // Instead of typically starting an XHR, we trigger the unsubscription from outside + setTimeout(() => subscription?.unsubscribe(), 5); + + return () => { + expect(batcher["batchesByKey"].get("")).toBeUndefined(); + done(); + }; + }), + }); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const subscription = batcher - .enqueueRequest({ - operation: createOperation({}, { query }), - }) - .subscribe(() => reject("next should never be called")); + const subscription = batcher + .enqueueRequest({ + operation: createOperation({}, { query }), + }) + .subscribe(() => { + throw new Error("next should never be called"); + }); - jest.runAllTimers(); - } - ); + jest.runAllTimers(); + }); - itAsync("should correctly batch multiple queries", (resolve, reject) => { + it("should correctly batch multiple queries", async () => { const data = { lastName: "Ever", firstName: "Greatest", @@ -646,126 +550,109 @@ describe("OperationBatcher", () => { batcher.enqueueRequest({ operation }).subscribe({}); batcher.enqueueRequest({ operation: operation2 }).subscribe({}); - try { - expect(batcher["batchesByKey"].get("")!.size).toBe(2); - } catch (e) { - reject(e); - } + expect(batcher["batchesByKey"].get("")!.size).toBe(2); setTimeout(() => { // The batch shouldn't be fired yet, so we can add one more request. batcher.enqueueRequest({ operation: operation3 }).subscribe({}); - try { - expect(batcher["batchesByKey"].get("")!.size).toBe(3); - } catch (e) { - reject(e); - } + expect(batcher["batchesByKey"].get("")!.size).toBe(3); }, 5); - setTimeout( - terminatingCheck(resolve, reject, () => { - // The batch should've been fired by now. - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - }), - 20 - ); - + const promise = wait(20); jest.runAllTimers(); + await promise; + + // The batch should've been fired by now. + expect(batcher["batchesByKey"].get("")).toBeUndefined(); }); - itAsync( - "should cancel multiple queries in queue when unsubscribing and let pass still subscribed one", - (resolve, reject) => { - const data2 = { - lastName: "Hauser", - firstName: "Evans", - }; + it("should cancel multiple queries in queue when unsubscribing and let pass still subscribed one", (done) => { + const data2 = { + lastName: "Hauser", + firstName: "Evans", + }; - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable((observer) => { - observer.next([{ data: data2 }]); - setTimeout(observer.complete.bind(observer)); - }), - }); + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable((observer) => { + observer.next([{ data: data2 }]); + setTimeout(observer.complete.bind(observer)); + }), + }); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; + + const operation: Operation = createOperation({}, { query }); + const operation2: Operation = createOperation({}, { query }); + const operation3: Operation = createOperation({}, { query }); - const operation: Operation = createOperation({}, { query }); - const operation2: Operation = createOperation({}, { query }); - const operation3: Operation = createOperation({}, { query }); + const sub1 = batcher.enqueueRequest({ operation }).subscribe(() => { + throw new Error("next should never be called"); + }); + batcher.enqueueRequest({ operation: operation2 }).subscribe((result) => { + expect(result.data).toBe(data2); - const sub1 = batcher - .enqueueRequest({ operation }) - .subscribe(() => reject("next should never be called")); - batcher.enqueueRequest({ operation: operation2 }).subscribe((result) => { - expect(result.data).toBe(data2); + // The batch should've been fired by now. + expect(batcher["batchesByKey"].get("")).toBeUndefined(); - // The batch should've been fired by now. - expect(batcher["batchesByKey"].get("")).toBeUndefined(); + done(); + }); - resolve(); - }); + expect(batcher["batchesByKey"].get("")!.size).toBe(2); + sub1.unsubscribe(); + expect(batcher["batchesByKey"].get("")!.size).toBe(1); + + setTimeout(() => { + // The batch shouldn't be fired yet, so we can add one more request. + const sub3 = batcher + .enqueueRequest({ operation: operation3 }) + .subscribe(() => { + throw new Error("next should never be called"); + }); expect(batcher["batchesByKey"].get("")!.size).toBe(2); - sub1.unsubscribe(); + sub3.unsubscribe(); expect(batcher["batchesByKey"].get("")!.size).toBe(1); + }, 5); - setTimeout(() => { - // The batch shouldn't be fired yet, so we can add one more request. - const sub3 = batcher - .enqueueRequest({ operation: operation3 }) - .subscribe(() => reject("next should never be called")); - expect(batcher["batchesByKey"].get("")!.size).toBe(2); - - sub3.unsubscribe(); - expect(batcher["batchesByKey"].get("")!.size).toBe(1); - }, 5); + jest.runAllTimers(); + }); - jest.runAllTimers(); - } - ); - - itAsync( - "should reject the promise if there is a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should reject the promise if there is a network error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const operation: Operation = createOperation({}, { query }); - const error = new Error("Network error"); - const BH = createMockBatchHandler({ - request: { query }, - error, - }); - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: BH, - }); + } + `; + const operation: Operation = createOperation({}, { query }); + const error = new Error("Network error"); + const BH = createMockBatchHandler({ + request: { query }, + error, + }); + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: BH, + }); - const observable = batcher.enqueueRequest({ operation }); - observable.subscribe({ - error: terminatingCheck(resolve, reject, (resError: Error) => { - expect(resError.message).toBe("Network error"); - }), - }); - batcher.consumeQueue(); - } - ); + const observable = batcher.enqueueRequest({ operation }); + const stream = new ObservableStream(observable); + batcher.consumeQueue(); + + await expect(stream).toEmitError(error); + }); }); describe("BatchLink", () => { @@ -781,25 +668,21 @@ describe("BatchLink", () => { ).not.toThrow(); }); - itAsync("passes forward on", (resolve, reject) => { + it("passes forward on", async () => { + expect.assertions(3); const link = ApolloLink.from([ new BatchLink({ batchInterval: 0, batchMax: 1, batchHandler: (operation, forward) => { - try { - expect(forward!.length).toBe(1); - expect(operation.length).toBe(1); - } catch (e) { - reject(e); - } + expect(forward!.length).toBe(1); + expect(operation.length).toBe(1); + return forward![0]!(operation[0]).map((result) => [result]); }, }), new ApolloLink((operation) => { - terminatingCheck(resolve, reject, () => { - expect(operation.query).toEqual(query); - })(); + expect(operation.query).toEqual(query); return null; }), ]); @@ -812,7 +695,7 @@ describe("BatchLink", () => { query, } ) - ).subscribe((result) => reject()); + ).subscribe(() => {}); }); it("raises warning if terminating", () => { @@ -849,28 +732,17 @@ describe("BatchLink", () => { expect(calls).toBe(2); }); - itAsync("correctly uses batch size", (resolve, reject) => { + it("correctly uses batch size", async () => { const sizes = [1, 2, 3]; const terminating = new ApolloLink((operation) => { - try { - expect(operation.query).toEqual(query); - } catch (e) { - reject(e); - } + expect(operation.query).toEqual(query); return Observable.of(operation.variables.count); }); - let runBatchSize = () => { - const size = sizes.pop(); - if (!size) resolve(); - + let runBatchSize = async (size: number) => { const batchHandler = jest.fn((operation, forward) => { - try { - expect(operation.length).toBe(size); - expect(forward.length).toBe(size); - } catch (e) { - reject(e); - } + expect(operation.length).toBe(size); + expect(forward.length).toBe(size); const observables = forward.map((f: any, i: any) => f(operation[i])); return new Observable((observer) => { const data: any[] = []; @@ -895,45 +767,43 @@ describe("BatchLink", () => { terminating, ]); - Array.from(new Array(size)).forEach((_, i) => { - execute(link, { - query, - variables: { count: i }, - }).subscribe({ - next: (data) => { - expect(data).toBe(i); - }, - complete: () => { - try { - expect(batchHandler.mock.calls.length).toBe(1); - } catch (e) { - reject(e); - } - runBatchSize(); - }, - }); - }); + return Promise.all( + Array.from(new Array(size)).map((_, i) => { + return new Promise((resolve) => { + execute(link, { + query, + variables: { count: i }, + }).subscribe({ + next: (data) => { + expect(data).toBe(i); + }, + complete: () => { + expect(batchHandler.mock.calls.length).toBe(1); + resolve(); + }, + }); + }); + }) + ); }; - runBatchSize(); + for (const size of sizes) { + await runBatchSize(size); + } }); - itAsync("correctly follows batch interval", (resolve, reject) => { + it("correctly follows batch interval", (done) => { const intervals = [10, 20, 30]; const runBatchInterval = () => { const mock = jest.fn(); const batchInterval = intervals.pop(); - if (!batchInterval) return resolve(); + if (!batchInterval) return done(); const batchHandler = jest.fn((operation, forward) => { - try { - expect(operation.length).toBe(1); - expect(forward.length).toBe(1); - } catch (e) { - reject(e); - } + expect(operation.length).toBe(1); + expect(forward.length).toBe(1); return forward[0](operation[0]).map((d: any) => [d]); }); @@ -957,11 +827,7 @@ describe("BatchLink", () => { ) ).subscribe({ next: (data) => { - try { - expect(data).toBe(42); - } catch (e) { - reject(e); - } + expect(data).toBe(42); }, complete: () => { mock(batchHandler.mock.calls.length); @@ -972,19 +838,15 @@ describe("BatchLink", () => { await delay(batchInterval); const checkCalls = mock.mock.calls.slice(0, -1); - try { - expect(checkCalls.length).toBe(2); - checkCalls.forEach((args) => expect(args[0]).toBe(0)); - expect(mock).lastCalledWith(1); - expect(batchHandler.mock.calls.length).toBe(1); - } catch (e) { - reject(e); - } + expect(checkCalls.length).toBe(2); + checkCalls.forEach((args) => expect(args[0]).toBe(0)); + expect(mock).lastCalledWith(1); + expect(batchHandler.mock.calls.length).toBe(1); runBatchInterval(); }; - delayedBatchInterval(); + void delayedBatchInterval(); mock(batchHandler.mock.calls.length); mock(batchHandler.mock.calls.length); @@ -994,97 +856,82 @@ describe("BatchLink", () => { runBatchInterval(); }); - itAsync( - "throws an error when more requests than results", - (resolve, reject) => { - const result = [{ data: {} }]; - const batchHandler = jest.fn((op) => Observable.of(result)); + it("throws an error when more requests than results", () => { + expect.assertions(4); + const result = [{ data: {} }]; + const batchHandler = jest.fn((op) => Observable.of(result)); + + const link = ApolloLink.from([ + new BatchLink({ + batchInterval: 10, + batchMax: 2, + batchHandler, + }), + ]); + + [1, 2].forEach((x) => { + execute(link, { + query, + }).subscribe({ + next: (data) => { + throw new Error("next should not be called"); + }, + error: (error: any) => { + expect(error).toBeDefined(); + expect(error.result).toEqual(result); + }, + complete: () => { + throw new Error("complete should not be called"); + }, + }); + }); + }); + + describe("batchKey", () => { + it("should allow different batches to be created separately", (done) => { + const data = { data: {} }; + const result = [data, data]; + + const batchHandler = jest.fn((op) => { + expect(op.length).toBe(2); + return Observable.of(result); + }); + let key = true; + const batchKey = () => { + key = !key; + return "" + !key; + }; const link = ApolloLink.from([ new BatchLink({ - batchInterval: 10, + batchInterval: 1, + //if batchKey does not work, then the batch size would be 3 batchMax: 2, batchHandler, + batchKey, }), ]); - [1, 2].forEach((x) => { + let count = 0; + [1, 2, 3, 4].forEach(() => { execute(link, { query, }).subscribe({ - next: (data) => { - reject("next should not be called"); + next: (d) => { + expect(d).toEqual(data); + }, + error: (e) => { + throw e; }, - error: terminatingCheck(resolve, reject, (error: any) => { - expect(error).toBeDefined(); - expect(error.result).toEqual(result); - }), complete: () => { - reject("complete should not be called"); + count++; + if (count === 4) { + expect(batchHandler.mock.calls.length).toBe(2); + done(); + } }, }); }); - } - ); - - describe("batchKey", () => { - itAsync( - "should allow different batches to be created separately", - (resolve, reject) => { - const data = { data: {} }; - const result = [data, data]; - - const batchHandler = jest.fn((op) => { - try { - expect(op.length).toBe(2); - } catch (e) { - reject(e); - } - return Observable.of(result); - }); - let key = true; - const batchKey = () => { - key = !key; - return "" + !key; - }; - - const link = ApolloLink.from([ - new BatchLink({ - batchInterval: 1, - //if batchKey does not work, then the batch size would be 3 - batchMax: 2, - batchHandler, - batchKey, - }), - ]); - - let count = 0; - [1, 2, 3, 4].forEach(() => { - execute(link, { - query, - }).subscribe({ - next: (d) => { - try { - expect(d).toEqual(data); - } catch (e) { - reject(e); - } - }, - error: reject, - complete: () => { - count++; - if (count === 4) { - try { - expect(batchHandler.mock.calls.length).toBe(2); - resolve(); - } catch (e) { - reject(e); - } - } - }, - }); - }); - } - ); + }); }); }); diff --git a/src/link/context/__tests__/index.ts b/src/link/context/__tests__/index.ts index 8aa7a6be03..c80be213a9 100644 --- a/src/link/context/__tests__/index.ts +++ b/src/link/context/__tests__/index.ts @@ -4,7 +4,8 @@ import { ApolloLink } from "../../core"; import { Observable } from "../../../utilities/observables/Observable"; import { execute } from "../../core/execute"; import { setContext } from "../index"; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const sleep = (ms: number) => new Promise((s) => setTimeout(s, ms)); const query = gql` @@ -18,68 +19,53 @@ const data = { foo: { bar: true }, }; -itAsync( - "can be used to set the context with a simple function", - (resolve, reject) => { - const withContext = setContext(() => ({ dynamicallySet: true })); +it("can be used to set the context with a simple function", async () => { + const withContext = setContext(() => ({ dynamicallySet: true })); - const mockLink = new ApolloLink((operation) => { - expect(operation.getContext().dynamicallySet).toBe(true); - return Observable.of({ data }); - }); + const mockLink = new ApolloLink((operation) => { + expect(operation.getContext().dynamicallySet).toBe(true); + return Observable.of({ data }); + }); - const link = withContext.concat(mockLink); + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); - } -); - -itAsync( - "can be used to set the context with a function returning a promise", - (resolve, reject) => { - const withContext = setContext(() => - Promise.resolve({ dynamicallySet: true }) - ); - - const mockLink = new ApolloLink((operation) => { - expect(operation.getContext().dynamicallySet).toBe(true); - return Observable.of({ data }); - }); + await expect(stream).toEmitValue({ data }); +}); - const link = withContext.concat(mockLink); +it("can be used to set the context with a function returning a promise", async () => { + const withContext = setContext(() => + Promise.resolve({ dynamicallySet: true }) + ); - execute(link, { query }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); - } -); - -itAsync( - "can be used to set the context with a function returning a promise that is delayed", - (resolve, reject) => { - const withContext = setContext(() => - sleep(25).then(() => ({ dynamicallySet: true })) - ); - - const mockLink = new ApolloLink((operation) => { - expect(operation.getContext().dynamicallySet).toBe(true); - return Observable.of({ data }); - }); + const mockLink = new ApolloLink((operation) => { + expect(operation.getContext().dynamicallySet).toBe(true); + return Observable.of({ data }); + }); - const link = withContext.concat(mockLink); + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); - } -); + await expect(stream).toEmitValue({ data }); +}); + +it("can be used to set the context with a function returning a promise that is delayed", async () => { + const withContext = setContext(() => + sleep(25).then(() => ({ dynamicallySet: true })) + ); + + const mockLink = new ApolloLink((operation) => { + expect(operation.getContext().dynamicallySet).toBe(true); + return Observable.of({ data }); + }); -itAsync("handles errors in the lookup correclty", (resolve, reject) => { + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); + + await expect(stream).toEmitValue({ data }); +}); + +it("handles errors in the lookup correclty", async () => { const withContext = setContext(() => sleep(5).then(() => { throw new Error("dang"); @@ -92,32 +78,27 @@ itAsync("handles errors in the lookup correclty", (resolve, reject) => { const link = withContext.concat(mockLink); - execute(link, { query }).subscribe(reject, (e) => { - expect(e.message).toBe("dang"); - resolve(); - }); + const stream = new ObservableStream(execute(link, { query })); + + await expect(stream).toEmitError("dang"); }); -itAsync( - "handles errors in the lookup correclty with a normal function", - (resolve, reject) => { - const withContext = setContext(() => { - throw new Error("dang"); - }); - const mockLink = new ApolloLink((operation) => { - return Observable.of({ data }); - }); +it("handles errors in the lookup correctly with a normal function", async () => { + const withContext = setContext(() => { + throw new Error("dang"); + }); - const link = withContext.concat(mockLink); + const mockLink = new ApolloLink((operation) => { + return Observable.of({ data }); + }); - execute(link, { query }).subscribe(reject, (e) => { - expect(e.message).toBe("dang"); - resolve(); - }); - } -); + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); -itAsync("has access to the request information", (resolve, reject) => { + await expect(stream).toEmitError("dang"); +}); + +it("has access to the request information", async () => { const withContext = setContext(({ operationName, query, variables }) => sleep(1).then(() => Promise.resolve({ @@ -137,13 +118,14 @@ itAsync("has access to the request information", (resolve, reject) => { }); const link = withContext.concat(mockLink); + const stream = new ObservableStream( + execute(link, { query, variables: { id: 1 } }) + ); - execute(link, { query, variables: { id: 1 } }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); + await expect(stream).toEmitValue({ data }); }); -itAsync("has access to the context at execution time", (resolve, reject) => { + +it("has access to the context at execution time", async () => { const withContext = setContext((_, { count }) => sleep(1).then(() => ({ count: count + 1 })) ); @@ -155,14 +137,14 @@ itAsync("has access to the context at execution time", (resolve, reject) => { }); const link = withContext.concat(mockLink); + const stream = new ObservableStream( + execute(link, { query, context: { count: 1 } }) + ); - execute(link, { query, context: { count: 1 } }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); + await expect(stream).toEmitValue({ data }); }); -itAsync("unsubscribes correctly", (resolve, reject) => { +it("unsubscribes correctly", async () => { const withContext = setContext((_, { count }) => sleep(1).then(() => ({ count: count + 1 })) ); @@ -175,18 +157,19 @@ itAsync("unsubscribes correctly", (resolve, reject) => { const link = withContext.concat(mockLink); - let handle = execute(link, { - query, - context: { count: 1 }, - }).subscribe((result) => { - expect(result.data).toEqual(data); - handle.unsubscribe(); - resolve(); - }); + const stream = new ObservableStream( + execute(link, { + query, + context: { count: 1 }, + }) + ); + + await expect(stream).toEmitValue({ data }); + stream.unsubscribe(); }); -itAsync("unsubscribes without throwing before data", (resolve, reject) => { - let called: boolean; +it("unsubscribes without throwing before data", async () => { + let called!: boolean; const withContext = setContext((_, { count }) => { called = true; return sleep(1).then(() => ({ count: count + 1 })); @@ -209,51 +192,43 @@ itAsync("unsubscribes without throwing before data", (resolve, reject) => { query, context: { count: 1 }, }).subscribe((result) => { - reject("should have unsubscribed"); + throw new Error("should have unsubscribed"); }); - setTimeout(() => { - handle.unsubscribe(); - expect(called).toBe(true); - resolve(); - }, 10); + await wait(10); + + handle.unsubscribe(); + expect(called).toBe(true); }); -itAsync( - "does not start the next link subscription if the upstream subscription is already closed", - (resolve, reject) => { - let promiseResolved = false; - const withContext = setContext(() => - sleep(5).then(() => { - promiseResolved = true; - return { dynamicallySet: true }; - }) - ); - - let mockLinkCalled = false; - const mockLink = new ApolloLink(() => { - mockLinkCalled = true; - reject("link should not be called"); - return new Observable((observer) => { - observer.error("link should not have been observed"); - }); - }); +it("does not start the next link subscription if the upstream subscription is already closed", async () => { + let promiseResolved = false; + const withContext = setContext(() => + sleep(5).then(() => { + promiseResolved = true; + return { dynamicallySet: true }; + }) + ); - const link = withContext.concat(mockLink); + let mockLinkCalled = false; + const mockLink = new ApolloLink(() => { + mockLinkCalled = true; + throw new Error("link should not be called"); + }); - let subscriptionReturnedData = false; - let handle = execute(link, { query }).subscribe((result) => { - subscriptionReturnedData = true; - reject("subscription should not return data"); - }); + const link = withContext.concat(mockLink); - handle.unsubscribe(); + let subscriptionReturnedData = false; + let handle = execute(link, { query }).subscribe((result) => { + subscriptionReturnedData = true; + throw new Error("subscription should not return data"); + }); - setTimeout(() => { - expect(promiseResolved).toBe(true); - expect(mockLinkCalled).toBe(false); - expect(subscriptionReturnedData).toBe(false); - resolve(); - }, 10); - } -); + handle.unsubscribe(); + + await wait(10); + + expect(promiseResolved).toBe(true); + expect(mockLinkCalled).toBe(false); + expect(subscriptionReturnedData).toBe(false); +}); diff --git a/src/link/error/__tests__/index.ts b/src/link/error/__tests__/index.ts index 5e886ff58d..0a3bf2bbfb 100644 --- a/src/link/error/__tests__/index.ts +++ b/src/link/error/__tests__/index.ts @@ -5,10 +5,10 @@ import { execute } from "../../core/execute"; import { ServerError, throwServerError } from "../../utils/throwServerError"; import { Observable } from "../../../utilities/observables/Observable"; import { onError, ErrorLink } from "../"; -import { itAsync } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; describe("error handling", () => { - itAsync("has an easy way to handle GraphQL errors", (resolve, reject) => { + it("has an easy way to handle GraphQL errors", async () => { const query = gql` { foo { @@ -17,7 +17,7 @@ describe("error handling", () => { } `; - let called: boolean; + let called = false; const errorLink = onError(({ graphQLErrors, networkError }) => { expect(graphQLErrors![0].message).toBe("resolver blew up"); called = true; @@ -34,47 +34,44 @@ describe("error handling", () => { ); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result.errors![0].message).toBe("resolver blew up"); - expect(called).toBe(true); - resolve(); - }); + const result = await stream.takeNext(); + + expect(result.errors![0].message).toBe("resolver blew up"); + expect(called).toBe(true); }); - itAsync( - "has an easy way to log client side (network) errors", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + + it("has an easy way to log client side (network) errors", async () => { + const query = gql` + query Foo { + foo { + bar } - `; + } + `; - let called: boolean; - const errorLink = onError(({ operation, networkError }) => { - expect(networkError!.message).toBe("app is crashing"); - expect(operation.operationName).toBe("Foo"); - called = true; - }); + let called = false; + const errorLink = onError(({ operation, networkError }) => { + expect(networkError!.message).toBe("app is crashing"); + expect(operation.operationName).toBe("Foo"); + called = true; + }); - const mockLink = new ApolloLink((operation) => { - throw new Error("app is crashing"); - }); + const mockLink = new ApolloLink((operation) => { + throw new Error("app is crashing"); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync("captures errors within links", (resolve, reject) => { + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); + }); + + it("captures errors within links", async () => { const query = gql` query Foo { foo { @@ -83,7 +80,7 @@ describe("error handling", () => { } `; - let called: boolean; + let called = false; const errorLink = onError(({ operation, networkError }) => { expect(networkError!.message).toBe("app is crashing"); expect(operation.operationName).toBe("Foo"); @@ -97,89 +94,79 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); }); - itAsync( - "captures networkError.statusCode within links", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + + it("captures networkError.statusCode within links", async () => { + const query = gql` + query Foo { + foo { + bar } - `; - - let called: boolean; - const errorLink = onError(({ operation, networkError }) => { - expect(networkError!.message).toBe("app is crashing"); - expect(networkError!.name).toBe("ServerError"); - expect((networkError as ServerError).statusCode).toBe(500); - expect((networkError as ServerError).response.ok).toBe(false); - expect(operation.operationName).toBe("Foo"); - called = true; - }); + } + `; - const mockLink = new ApolloLink((operation) => { - return new Observable((obs) => { - const response = { status: 500, ok: false } as Response; - throwServerError(response, "ServerError", "app is crashing"); - }); + let called = false; + const errorLink = onError(({ operation, networkError }) => { + expect(networkError!.message).toBe("app is crashing"); + expect(networkError!.name).toBe("ServerError"); + expect((networkError as ServerError).statusCode).toBe(500); + expect((networkError as ServerError).response.ok).toBe(false); + expect(operation.operationName).toBe("Foo"); + called = true; + }); + + const mockLink = new ApolloLink((operation) => { + return new Observable((obs) => { + const response = { status: 500, ok: false } as Response; + throwServerError(response, "ServerError", "app is crashing"); }); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync( - "sets graphQLErrors to undefined if networkError.result is an empty string", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); + }); + + it("sets graphQLErrors to undefined if networkError.result is an empty string", async () => { + const query = gql` + query Foo { + foo { + bar } - `; + } + `; - let called: boolean; - const errorLink = onError(({ graphQLErrors }) => { - expect(graphQLErrors).toBeUndefined(); - called = true; - }); + let called = false; + const errorLink = onError(({ graphQLErrors }) => { + expect(graphQLErrors).toBeUndefined(); + called = true; + }); - const mockLink = new ApolloLink((operation) => { - return new Observable((obs) => { - const response = { status: 500, ok: false } as Response; - throwServerError(response, "", "app is crashing"); - }); + const mockLink = new ApolloLink((operation) => { + return new Observable((obs) => { + const response = { status: 500, ok: false } as Response; + throwServerError(response, "", "app is crashing"); }); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync("completes if no errors", (resolve, reject) => { + await expect(stream).toEmitError(); + expect(called).toBe(true); + }); + + it("completes if no errors", async () => { const query = gql` { foo { @@ -197,12 +184,13 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - complete: resolve, - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("allows an error to be ignored", (resolve, reject) => { + + it("allows an error to be ignored", async () => { const query = gql` { foo { @@ -225,17 +213,16 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - next: ({ errors, data }) => { - expect(errors).toBe(null); - expect(data).toEqual({ foo: { id: 1 } }); - }, - complete: resolve, + await expect(stream).toEmitValue({ + errors: null, + data: { foo: { id: 1 } }, }); + await expect(stream).toComplete(); }); - itAsync("can be unsubcribed", (resolve, reject) => { + it("can be unsubcribed", async () => { const query = gql` { foo { @@ -258,62 +245,56 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - const sub = execute(link, { query }).subscribe({ - complete: () => { - reject("completed"); - }, - }); - - sub.unsubscribe(); + stream.unsubscribe(); - setTimeout(resolve, 10); + await expect(stream).not.toEmitAnything(); }); - itAsync( - "includes the operation and any data along with a graphql error", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + it("includes the operation and any data along with a graphql error", async () => { + const query = gql` + query Foo { + foo { + bar } - `; - - let called: boolean; - const errorLink = onError(({ graphQLErrors, response, operation }) => { - expect(graphQLErrors![0].message).toBe("resolver blew up"); - expect(response!.data!.foo).toBe(true); - expect(operation.operationName).toBe("Foo"); - expect(operation.getContext().bar).toBe(true); - called = true; - }); + } + `; - const mockLink = new ApolloLink((operation) => - Observable.of({ - data: { foo: true }, - errors: [ - { - message: "resolver blew up", - }, - ], - } as any) - ); - - const link = errorLink.concat(mockLink); - - execute(link, { query, context: { bar: true } }).subscribe((result) => { - expect(result.errors![0].message).toBe("resolver blew up"); - expect(called).toBe(true); - resolve(); - }); - } - ); + let called = false; + const errorLink = onError(({ graphQLErrors, response, operation }) => { + expect(graphQLErrors![0].message).toBe("resolver blew up"); + expect(response!.data!.foo).toBe(true); + expect(operation.operationName).toBe("Foo"); + expect(operation.getContext().bar).toBe(true); + called = true; + }); + + const mockLink = new ApolloLink((operation) => + Observable.of({ + data: { foo: true }, + errors: [ + { + message: "resolver blew up", + }, + ], + } as any) + ); + + const link = errorLink.concat(mockLink); + const stream = new ObservableStream( + execute(link, { query, context: { bar: true } }) + ); + + const result = await stream.takeNext(); + + expect(result.errors![0].message).toBe("resolver blew up"); + expect(called).toBe(true); + }); }); describe("error handling with class", () => { - itAsync("has an easy way to handle GraphQL errors", (resolve, reject) => { + it("has an easy way to handle GraphQL errors", async () => { const query = gql` { foo { @@ -322,7 +303,7 @@ describe("error handling with class", () => { } `; - let called: boolean; + let called = false; const errorLink = new ErrorLink(({ graphQLErrors, networkError }) => { expect(graphQLErrors![0].message).toBe("resolver blew up"); called = true; @@ -339,46 +320,43 @@ describe("error handling with class", () => { ); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result!.errors![0].message).toBe("resolver blew up"); - expect(called).toBe(true); - resolve(); - }); + const result = await stream.takeNext(); + + expect(result!.errors![0].message).toBe("resolver blew up"); + expect(called).toBe(true); }); - itAsync( - "has an easy way to log client side (network) errors", - (resolve, reject) => { - const query = gql` - { - foo { - bar - } + + it("has an easy way to log client side (network) errors", async () => { + const query = gql` + { + foo { + bar } - `; + } + `; - let called: boolean; - const errorLink = new ErrorLink(({ networkError }) => { - expect(networkError!.message).toBe("app is crashing"); - called = true; - }); + let called = false; + const errorLink = new ErrorLink(({ networkError }) => { + expect(networkError!.message).toBe("app is crashing"); + called = true; + }); - const mockLink = new ApolloLink((operation) => { - throw new Error("app is crashing"); - }); + const mockLink = new ApolloLink((operation) => { + throw new Error("app is crashing"); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync("captures errors within links", (resolve, reject) => { + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); + }); + + it("captures errors within links", async () => { const query = gql` { foo { @@ -387,7 +365,7 @@ describe("error handling with class", () => { } `; - let called: boolean; + let called = false; const errorLink = new ErrorLink(({ networkError }) => { expect(networkError!.message).toBe("app is crashing"); called = true; @@ -400,16 +378,15 @@ describe("error handling with class", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); }); - itAsync("completes if no errors", (resolve, reject) => { + + it("completes if no errors", async () => { const query = gql` { foo { @@ -428,11 +405,13 @@ describe("error handling with class", () => { const link = errorLink.concat(mockLink); - execute(link, { query }).subscribe({ - complete: resolve, - }); + const stream = new ObservableStream(execute(link, { query })); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("can be unsubcribed", (resolve, reject) => { + + it("can be unsubcribed", async () => { const query = gql` { foo { @@ -455,16 +434,11 @@ describe("error handling with class", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - const sub = execute(link, { query }).subscribe({ - complete: () => { - reject("completed"); - }, - }); - - sub.unsubscribe(); + stream.unsubscribe(); - setTimeout(resolve, 10); + await expect(stream).not.toEmitAnything(); }); }); @@ -491,118 +465,92 @@ describe("support for request retrying", () => { message: "some other error", }; - itAsync( - "returns the retried request when forward(operation) is called", - (resolve, reject) => { - let errorHandlerCalled = false; - - let timesCalled = 0; - const mockHttpLink = new ApolloLink((operation) => { - if (timesCalled === 0) { - timesCalled++; - // simulate the first request being an error - return new Observable((observer) => { - observer.next(ERROR_RESPONSE as any); - observer.complete(); - }); - } else { - return new Observable((observer) => { - observer.next(GOOD_RESPONSE); - observer.complete(); - }); - } - }); + it("returns the retried request when forward(operation) is called", async () => { + let errorHandlerCalled = false; - const errorLink = new ErrorLink( - ({ graphQLErrors, response, operation, forward }) => { - try { - if (graphQLErrors) { - errorHandlerCalled = true; - expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); - expect(response!.data).not.toBeDefined(); - expect(operation.operationName).toBe("Foo"); - expect(operation.getContext().bar).toBe(true); - // retry operation if it resulted in an error - return forward(operation); - } - } catch (error) { - reject(error); - } - } - ); - - const link = errorLink.concat(mockHttpLink); - - execute(link, { query: QUERY, context: { bar: true } }).subscribe({ - next(result) { - try { - expect(errorHandlerCalled).toBe(true); - expect(result).toEqual(GOOD_RESPONSE); - } catch (error) { - return reject(error); - } - }, - complete() { - resolve(); - }, - }); - } - ); - - itAsync( - "supports retrying when the initial request had networkError", - (resolve, reject) => { - let errorHandlerCalled = false; - - let timesCalled = 0; - const mockHttpLink = new ApolloLink((operation) => { - if (timesCalled === 0) { - timesCalled++; - // simulate the first request being an error - return new Observable((observer) => { - observer.error(NETWORK_ERROR); - }); - } else { - return new Observable((observer) => { - observer.next(GOOD_RESPONSE); - observer.complete(); - }); + let timesCalled = 0; + const mockHttpLink = new ApolloLink((operation) => { + if (timesCalled === 0) { + timesCalled++; + // simulate the first request being an error + return new Observable((observer) => { + observer.next(ERROR_RESPONSE as any); + observer.complete(); + }); + } else { + return new Observable((observer) => { + observer.next(GOOD_RESPONSE); + observer.complete(); + }); + } + }); + + const errorLink = new ErrorLink( + ({ graphQLErrors, response, operation, forward }) => { + if (graphQLErrors) { + errorHandlerCalled = true; + expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); + expect(response!.data).not.toBeDefined(); + expect(operation.operationName).toBe("Foo"); + expect(operation.getContext().bar).toBe(true); + // retry operation if it resulted in an error + return forward(operation); } - }); + } + ); + + const link = errorLink.concat(mockHttpLink); + + const stream = new ObservableStream( + execute(link, { query: QUERY, context: { bar: true } }) + ); + + await expect(stream).toEmitValue(GOOD_RESPONSE); + expect(errorHandlerCalled).toBe(true); + await expect(stream).toComplete(); + }); + + it("supports retrying when the initial request had networkError", async () => { + let errorHandlerCalled = false; - const errorLink = new ErrorLink( - ({ networkError, response, operation, forward }) => { - try { - if (networkError) { - errorHandlerCalled = true; - expect(networkError).toEqual(NETWORK_ERROR); - return forward(operation); - } - } catch (error) { - reject(error); - } + let timesCalled = 0; + const mockHttpLink = new ApolloLink((operation) => { + if (timesCalled === 0) { + timesCalled++; + // simulate the first request being an error + return new Observable((observer) => { + observer.error(NETWORK_ERROR); + }); + } else { + return new Observable((observer) => { + observer.next(GOOD_RESPONSE); + observer.complete(); + }); + } + }); + + const errorLink = new ErrorLink( + ({ networkError, response, operation, forward }) => { + if (networkError) { + errorHandlerCalled = true; + expect(networkError).toEqual(NETWORK_ERROR); + return forward(operation); } - ); - - const link = errorLink.concat(mockHttpLink); - - execute(link, { query: QUERY, context: { bar: true } }).subscribe({ - next(result) { - try { - expect(errorHandlerCalled).toBe(true); - expect(result).toEqual(GOOD_RESPONSE); - } catch (error) { - return reject(error); - } - }, - complete() { - resolve(); - }, - }); - } - ); + } + ); + + const link = errorLink.concat(mockHttpLink); + + const stream = new ObservableStream( + execute(link, { query: QUERY, context: { bar: true } }) + ); + + await expect(stream).toEmitValue(GOOD_RESPONSE); + expect(errorHandlerCalled).toBe(true); + await expect(stream).toComplete(); + }); - itAsync("returns errors from retried requests", (resolve, reject) => { + it("returns errors from retried requests", async () => { let errorHandlerCalled = false; let timesCalled = 0; @@ -623,38 +571,25 @@ describe("support for request retrying", () => { const errorLink = new ErrorLink( ({ graphQLErrors, networkError, response, operation, forward }) => { - try { - if (graphQLErrors) { - errorHandlerCalled = true; - expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); - expect(response!.data).not.toBeDefined(); - expect(operation.operationName).toBe("Foo"); - expect(operation.getContext().bar).toBe(true); - // retry operation if it resulted in an error - return forward(operation); - } - } catch (error) { - reject(error); + if (graphQLErrors) { + errorHandlerCalled = true; + expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); + expect(response!.data).not.toBeDefined(); + expect(operation.operationName).toBe("Foo"); + expect(operation.getContext().bar).toBe(true); + // retry operation if it resulted in an error + return forward(operation); } } ); const link = errorLink.concat(mockHttpLink); - let observerNextCalled = false; - execute(link, { query: QUERY, context: { bar: true } }).subscribe({ - next(result) { - // should not be called - observerNextCalled = true; - }, - error(error) { - // note that complete will not be after an error - // therefore we should end the test here with resolve() - expect(errorHandlerCalled).toBe(true); - expect(observerNextCalled).toBe(false); - expect(error).toEqual(NETWORK_ERROR); - resolve(); - }, - }); + const stream = new ObservableStream( + execute(link, { query: QUERY, context: { bar: true } }) + ); + + await expect(stream).toEmitError(NETWORK_ERROR); + expect(errorHandlerCalled).toBe(true); }); }); diff --git a/src/link/http/__tests__/HttpLink.ts b/src/link/http/__tests__/HttpLink.ts index ad58e4c40c..5d8e9a155d 100644 --- a/src/link/http/__tests__/HttpLink.ts +++ b/src/link/http/__tests__/HttpLink.ts @@ -19,7 +19,8 @@ import { ClientParseError } from "../serializeFetchParameter"; import { ServerParseError } from "../parseAndCheckHttpResponse"; import { FetchResult, ServerError } from "../../.."; import { voidFetchDuringEachTest } from "./helpers"; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const sampleQuery = gql` query SampleQuery { @@ -85,22 +86,6 @@ const sampleSubscriptionWithDefer = gql` } `; -function makeCallback( - resolve: () => void, - reject: (error: Error) => void, - callback: (...args: TArgs) => any -) { - return function () { - try { - // @ts-expect-error - callback.apply(this, arguments); - resolve(); - } catch (error) { - reject(error as Error); - } - } as typeof callback; -} - function convertBatchedBody(body: BodyInit | null | undefined) { return JSON.parse(body as string); } @@ -153,26 +138,18 @@ describe("HttpLink", () => { expect(() => new HttpLink()).not.toThrow(); }); - itAsync( - "constructor creates link that can call next and then complete", - (resolve, reject) => { - const next = jest.fn(); - const link = new HttpLink({ uri: "/data" }); - const observable = execute(link, { - query: sampleQuery, - }); - observable.subscribe({ - next, - error: (error) => expect(false), - complete: () => { - expect(next).toHaveBeenCalledTimes(1); - resolve(); - }, - }); - } - ); + it("constructor creates link that can call next and then complete", async () => { + const link = new HttpLink({ uri: "/data" }); + const observable = execute(link, { + query: sampleQuery, + }); + const stream = new ObservableStream(observable); - itAsync("supports using a GET request", (resolve, reject) => { + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + }); + + it("supports using a GET request", async () => { const variables = { params: "stub" }; const extensions = { myExtension: "foo" }; @@ -183,298 +160,290 @@ describe("HttpLink", () => { includeUnusedVariables: true, }); - execute(link, { query: sampleQuery, variables, extensions }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D&extensions=%7B%22myExtension%22%3A%22foo%22%7D" - ); - }), - error: (error) => reject(error), + const observable = execute(link, { + query: sampleQuery, + variables, + extensions, }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D&extensions=%7B%22myExtension%22%3A%22foo%22%7D" + ); }); - itAsync("supports using a GET request with search", (resolve, reject) => { + it("supports using a GET request with search", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data?foo=bar", fetchOptions: { method: "GET" }, }); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - execute(link, { query: sampleQuery, variables }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?foo=bar&query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" - ); - }), - error: (error) => reject(error), - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?foo=bar&query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" + ); }); - itAsync( - "supports using a GET request on the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - }); + it("supports using a GET request on the context", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + }); - execute(link, { - query: sampleQuery, - variables, - context: { - fetchOptions: { method: "GET" }, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" - ); - }) - ); - } - ); + const observable = execute(link, { + query: sampleQuery, + variables, + context: { + fetchOptions: { method: "GET" }, + }, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" + ); + }); - itAsync("uses GET with useGETForQueries", (resolve, reject) => { + it("uses GET with useGETForQueries", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data", useGETForQueries: true, }); - execute(link, { + const observable = execute(link, { query: sampleQuery, variables, - }).subscribe( - makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" - ); - }) + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" ); }); - itAsync( - "uses POST for mutations with useGETForQueries", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - useGETForQueries: true, - }); + it("uses POST for mutations with useGETForQueries", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + useGETForQueries: true, + }); - execute(link, { - query: sampleMutation, - variables, - }).subscribe( - makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeDefined(); - expect(method).toBe("POST"); - expect(uri).toBe("/data"); - }) - ); - } - ); - - itAsync( - "strips unused variables, respecting nested fragments", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - - const query = gql` - query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { - people(surprise: $undeclared, noSurprise: $declaredAndUsed) { - ... on Doctor { - specialty(var: $usedByInlineFragment) - } - ...LawyerFragment + const observable = execute(link, { + query: sampleMutation, + variables, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeDefined(); + expect(method).toBe("POST"); + expect(uri).toBe("/data"); + }); + + it("strips unused variables, respecting nested fragments", async () => { + const link = createHttpLink({ uri: "/data" }); + + const query = gql` + query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { + people(surprise: $undeclared, noSurprise: $declaredAndUsed) { + ... on Doctor { + specialty(var: $usedByInlineFragment) } + ...LawyerFragment } - fragment LawyerFragment on Lawyer { - caseCount(var: $usedByNamedFragment) - } - `; + } + fragment LawyerFragment on Lawyer { + caseCount(var: $usedByNamedFragment) + } + `; + + const variables = { + unused: "strip", + declaredButUnused: "strip", + declaredAndUsed: "keep", + undeclared: "keep", + usedByInlineFragment: "keep", + usedByNamedFragment: "keep", + }; + + const observable = execute(link, { + query, + variables, + }); + const stream = new ObservableStream(observable); - const variables = { - unused: "strip", - declaredButUnused: "strip", + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(JSON.parse(body as string)).toEqual({ + operationName: "PEOPLE", + query: print(query), + variables: { declaredAndUsed: "keep", undeclared: "keep", usedByInlineFragment: "keep", usedByNamedFragment: "keep", - }; - - execute(link, { - query, - variables, - }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(JSON.parse(body as string)).toEqual({ - operationName: "PEOPLE", - query: print(query), - variables: { - declaredAndUsed: "keep", - undeclared: "keep", - usedByInlineFragment: "keep", - usedByNamedFragment: "keep", - }, - }); - expect(method).toBe("POST"); - expect(uri).toBe("/data"); - }), - error: (error) => reject(error), - }); - } - ); + }, + }); + expect(method).toBe("POST"); + expect(uri).toBe("/data"); + }); - itAsync( - "should add client awareness settings to request headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - }); + it("should add client awareness settings to request headers", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + }); - const clientAwareness = { - name: "Some Client Name", - version: "1.0.1", - }; + const clientAwareness = { + name: "Some Client Name", + version: "1.0.1", + }; - execute(link, { - query: sampleQuery, - variables, - context: { - clientAwareness, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - const [, options] = fetchMock.lastCall()!; - const { headers } = options as any; - expect(headers["apollographql-client-name"]).toBeDefined(); - expect(headers["apollographql-client-name"]).toEqual( - clientAwareness.name - ); - expect(headers["apollographql-client-version"]).toBeDefined(); - expect(headers["apollographql-client-version"]).toEqual( - clientAwareness.version - ); - }) - ); - } - ); + const observable = execute(link, { + query: sampleQuery, + variables, + context: { + clientAwareness, + }, + }); + const stream = new ObservableStream(observable); - itAsync( - "should not add empty client awareness settings to request headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); - const hasOwn = Object.prototype.hasOwnProperty; - const clientAwareness = {}; - execute(link, { - query: sampleQuery, - variables, - context: { - clientAwareness, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - const [, options] = fetchMock.lastCall()!; - const { headers } = options as any; - expect(hasOwn.call(headers, "apollographql-client-name")).toBe( - false - ); - expect(hasOwn.call(headers, "apollographql-client-version")).toBe( - false - ); - }) - ); - } - ); + const [, options] = fetchMock.lastCall()!; + const { headers } = options as any; + expect(headers["apollographql-client-name"]).toBeDefined(); + expect(headers["apollographql-client-name"]).toEqual( + clientAwareness.name + ); + expect(headers["apollographql-client-version"]).toBeDefined(); + expect(headers["apollographql-client-version"]).toEqual( + clientAwareness.version + ); + }); - itAsync( - "throws for GET if the variables can't be stringified", - (resolve, reject) => { - const link = createHttpLink({ - uri: "/data", - useGETForQueries: true, - includeUnusedVariables: true, - }); + it("should not add empty client awareness settings to request headers", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + }); - let b; - const a: any = { b }; - b = { a }; - a.b = b; - const variables = { - a, - b, - }; - execute(link, { query: sampleQuery, variables }).subscribe( - (result) => { - reject("next should have been thrown from the link"); - }, - makeCallback(resolve, reject, (e: ClientParseError) => { - expect(e.message).toMatch(/Variables map is not serializable/); - expect(e.parseError.message).toMatch( - /Converting circular structure to JSON/ - ); - }) - ); - } - ); + const hasOwn = Object.prototype.hasOwnProperty; + const clientAwareness = {}; + const observable = execute(link, { + query: sampleQuery, + variables, + context: { + clientAwareness, + }, + }); + const stream = new ObservableStream(observable); - itAsync( - "throws for GET if the extensions can't be stringified", - (resolve, reject) => { - const link = createHttpLink({ - uri: "/data", - useGETForQueries: true, - includeExtensions: true, - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); - let b; - const a: any = { b }; - b = { a }; - a.b = b; - const extensions = { - a, - b, - }; - execute(link, { query: sampleQuery, extensions }).subscribe( - (result) => { - reject("next should have been thrown from the link"); - }, - makeCallback(resolve, reject, (e: ClientParseError) => { - expect(e.message).toMatch(/Extensions map is not serializable/); - expect(e.parseError.message).toMatch( - /Converting circular structure to JSON/ - ); - }) - ); - } - ); + const [, options] = fetchMock.lastCall()!; + const { headers } = options as any; + expect(hasOwn.call(headers, "apollographql-client-name")).toBe(false); + expect(hasOwn.call(headers, "apollographql-client-version")).toBe(false); + }); + + it("throws for GET if the variables can't be stringified", async () => { + const link = createHttpLink({ + uri: "/data", + useGETForQueries: true, + includeUnusedVariables: true, + }); + + let b; + const a: any = { b }; + b = { a }; + a.b = b; + const variables = { + a, + b, + }; + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Variables map is not serializable/); + expect(error.parseError.message).toMatch( + /Converting circular structure to JSON/ + ); + }); + + it("throws for GET if the extensions can't be stringified", async () => { + const link = createHttpLink({ + uri: "/data", + useGETForQueries: true, + includeExtensions: true, + }); + + let b; + const a: any = { b }; + b = { a }; + a.b = b; + const extensions = { + a, + b, + }; + const observable = execute(link, { query: sampleQuery, extensions }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Extensions map is not serializable/); + expect(error.parseError.message).toMatch( + /Converting circular structure to JSON/ + ); + }); it("raises warning if called with concat", () => { const link = createHttpLink(); @@ -494,71 +463,65 @@ describe("HttpLink", () => { expect(() => createHttpLink()).not.toThrow(); }); - itAsync("calls next and then complete", (resolve, reject) => { - const next = jest.fn(); + it("calls next and then complete", async () => { const link = createHttpLink({ uri: "data" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: makeCallback(resolve, reject, () => { - expect(next).toHaveBeenCalledTimes(1); - }), - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "error" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: TypeError) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "error" }); const observable = execute(link, { query: sampleMutation, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: TypeError) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync("unsubscribes without calling subscriber", (resolve, reject) => { + it("unsubscribes without calling subscriber", async () => { const link = createHttpLink({ uri: "data" }); const observable = execute(link, { query: sampleQuery, }); const subscription = observable.subscribe( - (result) => reject("next should not have been called"), - (error) => reject(error), - () => reject("complete should not have been called") + () => { + throw new Error("next should not have been called"); + }, + (error) => { + throw error; + }, + () => { + throw "complete should not have been called"; + } ); subscription.unsubscribe(); + expect(subscription.closed).toBe(true); - setTimeout(resolve, 50); + + // Ensure none of the callbacks throw after our assertion + await wait(10); }); - const verifyRequest = ( + const verifyRequest = async ( link: ApolloLink, - resolve: () => void, - includeExtensions: boolean, - reject: (error: any) => any + includeExtensions: boolean ) => { - const next = jest.fn(); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -567,57 +530,37 @@ describe("HttpLink", () => { context, variables, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: () => { - try { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); - expect(body.query).toBe(print(sampleMutation)); - expect(body.variables).toEqual({}); - expect(body.context).not.toBeDefined(); - if (includeExtensions) { - expect(body.extensions).toBeDefined(); - } else { - expect(body.extensions).not.toBeDefined(); - } - expect(next).toHaveBeenCalledTimes(1); - - resolve(); - } catch (e) { - reject(e); - } - }, - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + expect(body.query).toBe(print(sampleMutation)); + expect(body.variables).toEqual({}); + expect(body.context).not.toBeDefined(); + if (includeExtensions) { + expect(body.extensions).toBeDefined(); + } else { + expect(body.extensions).not.toBeDefined(); + } }; - itAsync( - "passes all arguments to multiple fetch body including extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "data", includeExtensions: true }); - verifyRequest( - link, - () => verifyRequest(link, resolve, true, reject), - true, - reject - ); - } - ); + it("passes all arguments to multiple fetch body including extensions", async () => { + const link = createHttpLink({ uri: "data", includeExtensions: true }); - itAsync( - "passes all arguments to multiple fetch body excluding extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "data" }); - verifyRequest( - link, - () => verifyRequest(link, resolve, false, reject), - false, - reject - ); - } - ); + await verifyRequest(link, true); + await verifyRequest(link, true); + }); + + it("passes all arguments to multiple fetch body excluding extensions", async () => { + const link = createHttpLink({ uri: "data" }); + + await verifyRequest(link, false); + await verifyRequest(link, false); + }); - itAsync("calls multiple subscribers", (resolve, reject) => { + it("calls multiple subscribers", async () => { const link = createHttpLink({ uri: "data" }); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -630,159 +573,144 @@ describe("HttpLink", () => { observable.subscribe(subscriber); observable.subscribe(subscriber); - setTimeout(() => { - expect(subscriber.next).toHaveBeenCalledTimes(2); - expect(subscriber.complete).toHaveBeenCalledTimes(2); - expect(subscriber.error).not.toHaveBeenCalled(); - expect(fetchMock.calls().length).toBe(2); - resolve(); - }, 50); - }); - - itAsync( - "calls remaining subscribers after unsubscribe", - (resolve, reject) => { - const link = createHttpLink({ uri: "data" }); - const context = { info: "stub" }; - const variables = { params: "stub" }; - - const observable = execute(link, { - query: sampleMutation, - context, - variables, - }); + await wait(50); - observable.subscribe(subscriber); + expect(subscriber.next).toHaveBeenCalledTimes(2); + expect(subscriber.complete).toHaveBeenCalledTimes(2); + expect(subscriber.error).not.toHaveBeenCalled(); + expect(fetchMock.calls().length).toBe(2); + }); - setTimeout(() => { - const subscription = observable.subscribe(subscriber); - subscription.unsubscribe(); - }, 10); + it("calls remaining subscribers after unsubscribe", async () => { + const link = createHttpLink({ uri: "data" }); + const context = { info: "stub" }; + const variables = { params: "stub" }; - setTimeout( - makeCallback(resolve, reject, () => { - expect(subscriber.next).toHaveBeenCalledTimes(1); - expect(subscriber.complete).toHaveBeenCalledTimes(1); - expect(subscriber.error).not.toHaveBeenCalled(); - resolve(); - }), - 50 - ); - } - ); + const observable = execute(link, { + query: sampleMutation, + context, + variables, + }); + + observable.subscribe(subscriber); + + await wait(10); + + const subscription = observable.subscribe(subscriber); + subscription.unsubscribe(); + + await wait(50); - itAsync("allows for dynamic endpoint setting", (resolve, reject) => { + expect(subscriber.next).toHaveBeenCalledTimes(1); + expect(subscriber.complete).toHaveBeenCalledTimes(1); + expect(subscriber.error).not.toHaveBeenCalled(); + }); + + it("allows for dynamic endpoint setting", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data" }); - execute(link, { + const observable = execute(link, { query: sampleQuery, variables, context: { uri: "data2" }, - }).subscribe((result) => { - expect(result).toEqual(data2); - resolve(); }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(data2); }); - itAsync( - "adds headers to the request from the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { authorization: "1234" }, - }); - return forward(operation).map((result) => { - const { headers } = operation.getContext(); - try { - expect(headers).toBeDefined(); - } catch (e) { - reject(e); - } - return result; - }); + it("adds headers to the request from the context", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + headers: { authorization: "1234" }, }); - const link = middleware.concat(createHttpLink({ uri: "data" })); - - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + return forward(operation).map((result) => { + const { headers } = operation.getContext(); + expect(headers).toBeDefined(); - itAsync("adds headers to the request from the setup", (resolve, reject) => { + return result; + }); + }); + const link = middleware.concat(createHttpLink({ uri: "data" })); + + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const headers = fetchMock.lastCall()![1]!.headers as any; + + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds headers to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data", headers: { authorization: "1234" }, }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const headers = fetchMock.lastCall()![1]!.headers as any; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); }); - itAsync( - "prioritizes context headers over setup headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { authorization: "1234" }, - }); - return forward(operation); + it("prioritizes context headers over setup headers", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + headers: { authorization: "1234" }, }); - const link = middleware.concat( - createHttpLink({ uri: "data", headers: { authorization: "no user" } }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "data", headers: { authorization: "no user" } }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - itAsync( - "adds headers to the request from the context on an operation", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ uri: "data" }); + await expect(stream).toEmitNext(); - const context = { - headers: { authorization: "1234" }, - }; - execute(link, { - query: sampleQuery, - variables, - context, - }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + const headers = fetchMock.lastCall()![1]!.headers as any; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); - itAsync("adds creds to the request from the context", (resolve, reject) => { + it("adds headers to the request from the context on an operation", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ uri: "data" }); + + const context = { + headers: { authorization: "1234" }, + }; + const observable = execute(link, { + query: sampleQuery, + variables, + context, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const headers = fetchMock.lastCall()![1]!.headers as any; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds creds to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -792,50 +720,50 @@ describe("HttpLink", () => { }); const link = middleware.concat(createHttpLink({ uri: "data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync("adds creds to the request from the setup", (resolve, reject) => { + it("adds creds to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data", credentials: "same-team-yo" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync( - "prioritizes creds from the context over the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - credentials: "same-team-yo", - }); - return forward(operation); + it("prioritizes creds from the context over the setup", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + credentials: "same-team-yo", }); - const link = middleware.concat( - createHttpLink({ uri: "data", credentials: "error" }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "data", credentials: "error" }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); - } - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); - itAsync("adds uri to the request from the context", (resolve, reject) => { + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); + }); + + it("adds uri to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -845,27 +773,29 @@ describe("HttpLink", () => { }); const link = middleware.concat(createHttpLink()); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("adds uri to the request from the setup", (resolve, reject) => { + it("adds uri to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("prioritizes context uri over setup uri", (resolve, reject) => { + it("prioritizes context uri over setup uri", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -877,168 +807,139 @@ describe("HttpLink", () => { createHttpLink({ uri: "data", credentials: "error" }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const uri = fetchMock.lastUrl(); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - expect(uri).toBe("/apollo"); - }) - ); + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/apollo"); }); - itAsync("allows uri to be a function", (resolve, reject) => { + it("allows uri to be a function", async () => { const variables = { params: "stub" }; const customFetch: typeof fetch = (uri, options) => { const { operationName } = convertBatchedBody(options!.body); - try { - expect(operationName).toBe("SampleQuery"); - } catch (e) { - reject(e); - } + expect(operationName).toBe("SampleQuery"); + return fetch("dataFunc", options); }; const link = createHttpLink({ fetch: customFetch }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetchMock.lastUrl()).toBe("/dataFunc"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + expect(fetchMock.lastUrl()).toBe("/dataFunc"); }); - itAsync( - "adds fetchOptions to the request from the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "data", - fetchOptions: { someOption: "foo", mode: "no-cors" }, - }); + it("adds fetchOptions to the request from the setup", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "data", + fetchOptions: { someOption: "foo", mode: "no-cors" }, + }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const { someOption, mode, headers } = - fetchMock.lastCall()![1] as any; - expect(someOption).toBe("foo"); - expect(mode).toBe("no-cors"); - expect(headers["content-type"]).toBe("application/json"); - }) - ); - } - ); - - itAsync( - "adds fetchOptions to the request from the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - fetchOptions: { - someOption: "foo", - }, - }); - return forward(operation); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const { someOption, mode, headers } = fetchMock.lastCall()![1] as any; + expect(someOption).toBe("foo"); + expect(mode).toBe("no-cors"); + expect(headers["content-type"]).toBe("application/json"); + }); + + it("adds fetchOptions to the request from the context", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + fetchOptions: { + someOption: "foo", + }, }); - const link = middleware.concat(createHttpLink({ uri: "data" })); + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const { someOption } = fetchMock.lastCall()![1] as any; - expect(someOption).toBe("foo"); - resolve(); - }) - ); - } - ); - - itAsync( - "uses the latest window.fetch function if options.fetch not configured", - (resolve, reject) => { - const httpLink = createHttpLink({ uri: "data" }); - - const fetch = window.fetch; - expect(typeof fetch).toBe("function"); - - const fetchSpy = jest.spyOn(window, "fetch"); - fetchSpy.mockImplementation(() => - Promise.resolve({ - text() { - return Promise.resolve( - JSON.stringify({ - data: { hello: "from spy" }, - }) - ); - }, - } as Response) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - const spyFn = window.fetch; - expect(spyFn).not.toBe(fetch); + await expect(stream).toEmitNext(); - subscriptions.add( - execute(httpLink, { - query: sampleQuery, - }).subscribe({ - error: reject, + const { someOption } = fetchMock.lastCall()![1] as any; + expect(someOption).toBe("foo"); + }); - next(result) { - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - data: { hello: "from spy" }, - }); - - fetchSpy.mockRestore(); - expect(window.fetch).toBe(fetch); - - subscriptions.add( - execute(httpLink, { - query: sampleQuery, - }).subscribe({ - error: reject, - next(result) { - expect(result).toEqual({ - data: { hello: "world" }, - }); - resolve(); - }, - }) - ); - }, - }) - ); - } - ); - - itAsync( - "uses the print option function when defined", - (resolve, reject) => { - const customPrinter = jest.fn( - (ast: ASTNode, originalPrint: typeof print) => { - return stripIgnoredCharacters(originalPrint(ast)); - } - ); + it("uses the latest window.fetch function if options.fetch not configured", async () => { + const httpLink = createHttpLink({ uri: "data" }); - const httpLink = createHttpLink({ uri: "data", print: customPrinter }); + const fetch = window.fetch; + expect(typeof fetch).toBe("function"); - execute(httpLink, { - query: sampleQuery, - context: { - fetchOptions: { method: "GET" }, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(customPrinter).toHaveBeenCalledTimes(1); - const [uri] = fetchMock.lastCall()!; - expect(uri).toBe( - "/data?query=query%20SampleQuery%7Bstub%7Bid%7D%7D&operationName=SampleQuery&variables=%7B%7D" + const fetchSpy = jest.spyOn(window, "fetch"); + fetchSpy.mockImplementation(() => + Promise.resolve({ + text() { + return Promise.resolve( + JSON.stringify({ + data: { hello: "from spy" }, + }) ); - }) - ); - } - ); + }, + } as Response) + ); + + const spyFn = window.fetch; + expect(spyFn).not.toBe(fetch); + + const stream = new ObservableStream( + execute(httpLink, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: { hello: "from spy" } }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + + fetchSpy.mockRestore(); + expect(window.fetch).toBe(fetch); + + const stream2 = new ObservableStream( + execute(httpLink, { query: sampleQuery }) + ); + + await expect(stream2).toEmitValue({ data: { hello: "world" } }); + }); + + it("uses the print option function when defined", async () => { + const customPrinter = jest.fn( + (ast: ASTNode, originalPrint: typeof print) => { + return stripIgnoredCharacters(originalPrint(ast)); + } + ); + + const httpLink = createHttpLink({ uri: "data", print: customPrinter }); + + const observable = execute(httpLink, { + query: sampleQuery, + context: { + fetchOptions: { method: "GET" }, + }, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); - itAsync("prioritizes context over setup", (resolve, reject) => { + expect(customPrinter).toHaveBeenCalledTimes(1); + const [uri] = fetchMock.lastCall()!; + expect(uri).toBe( + "/data?query=query%20SampleQuery%7Bstub%7Bid%7D%7D&operationName=SampleQuery&variables=%7B%7D" + ); + }); + + it("prioritizes context over setup", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -1052,55 +953,53 @@ describe("HttpLink", () => { createHttpLink({ uri: "data", fetchOptions: { someOption: "bar" } }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const { someOption } = fetchMock.lastCall()![1] as any; - expect(someOption).toBe("foo"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const { someOption } = fetchMock.lastCall()![1] as any; + expect(someOption).toBe("foo"); }); - itAsync( - "allows for not sending the query with the request", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - http: { - includeQuery: false, - includeExtensions: true, - }, - }); - operation.extensions.persistedQuery = { hash: "1234" }; - return forward(operation); + it("allows for not sending the query with the request", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + http: { + includeQuery: false, + includeExtensions: true, + }, }); - const link = middleware.concat(createHttpLink({ uri: "data" })); + operation.extensions.persistedQuery = { hash: "1234" }; + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - expect(body.query).not.toBeDefined(); - expect(body.extensions).toEqual({ - persistedQuery: { hash: "1234" }, - }); - resolve(); - }) - ); - } - ); + await expect(stream).toEmitNext(); + + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); - itAsync("sets the raw response on context", (resolve, reject) => { + expect(body.query).not.toBeDefined(); + expect(body.extensions).toEqual({ + persistedQuery: { hash: "1234" }, + }); + }); + + it("sets the raw response on context", async () => { const middleware = new ApolloLink((operation, forward) => { return new Observable((ob) => { const op = forward(operation); const sub = op.subscribe({ next: ob.next.bind(ob), error: ob.error.bind(ob), - complete: makeCallback(resolve, reject, () => { + complete: () => { expect(operation.getContext().response.headers.toBeDefined); ob.complete(); - }), + }, }); return () => { @@ -1111,12 +1010,11 @@ describe("HttpLink", () => { const link = middleware.concat(createHttpLink({ uri: "data", fetch })); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - resolve(); - }, - () => {} - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); it("removes @client fields from the query before sending it to the server", async () => { @@ -1195,26 +1093,22 @@ describe("HttpLink", () => { describe("Dev warnings", () => { voidFetchDuringEachTest(); - itAsync("warns if fetch is undeclared", (resolve, reject) => { + it("warns if fetch is undeclared", async () => { try { createHttpLink({ uri: "data" }); - reject("warning wasn't called"); + throw new Error("warning wasn't called"); } catch (e) { - makeCallback(resolve, reject, () => - expect((e as Error).message).toMatch(/has not been found globally/) - )(); + expect((e as Error).message).toMatch(/has not been found globally/); } }); - itAsync("warns if fetch is undefined", (resolve, reject) => { + it("warns if fetch is undefined", async () => { window.fetch = undefined as any; try { createHttpLink({ uri: "data" }); - reject("warning wasn't called"); + throw new Error("warning wasn't called"); } catch (e) { - makeCallback(resolve, reject, () => - expect((e as Error).message).toMatch(/has not been found globally/) - )(); + expect((e as Error).message).toMatch(/has not been found globally/); } }); @@ -1259,18 +1153,18 @@ describe("HttpLink", () => { beforeEach(() => { fetch.mockReset(); }); - itAsync("makes it easy to do stuff on a 401", (resolve, reject) => { + it("makes it easy to do stuff on a 401", async () => { const middleware = new ApolloLink((operation, forward) => { return new Observable((ob) => { fetch.mockReturnValueOnce(Promise.resolve({ status: 401, text })); const op = forward(operation); const sub = op.subscribe({ next: ob.next.bind(ob), - error: makeCallback(resolve, reject, (e: ServerError) => { + error: (e: ServerError) => { expect(e.message).toMatch(/Received status code 401/); expect(e.statusCode).toEqual(401); ob.error(e); - }), + }, complete: ob.complete.bind(ob), }); @@ -1284,115 +1178,94 @@ describe("HttpLink", () => { createHttpLink({ uri: "data", fetch: fetch as any }) ); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - () => {} - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(); }); - itAsync("throws an error if response code is > 300", (resolve, reject) => { + it("throws an error if response code is > 300", async () => { fetch.mockReturnValueOnce(Promise.resolve({ status: 400, text })); const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerError) => { - expect(e.message).toMatch(/Received status code 400/); - expect(e.statusCode).toBe(400); - expect(e.result).toEqual(responseBody); - }) + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error: ServerError = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 400/); + expect(error.statusCode).toBe(400); + expect(error.result).toEqual(responseBody); + }); + + it("throws an error if response code is > 300 and handles string response body", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 302, text: textWithStringError }) ); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error: ServerError = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 302/); + expect(error.statusCode).toBe(302); + expect(error.result).toEqual(responseBody); }); - itAsync( - "throws an error if response code is > 300 and handles string response body", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 302, text: textWithStringError }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerError) => { - expect(e.message).toMatch(/Received status code 302/); - expect(e.statusCode).toBe(302); - expect(e.result).toEqual(responseBody); - }) - ); - } - ); - itAsync( - "throws an error if response code is > 300 and returns data", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 400, text: textWithData }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + it("throws an error if response code is > 300 and returns data", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 400, text: textWithData }) + ); - let called = false; + const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - called = true; - expect(result).toEqual(responseBody); - }, - (e) => { - expect(called).toBe(true); - expect(e.message).toMatch(/Received status code 400/); - expect(e.statusCode).toBe(400); - expect(e.result).toEqual(responseBody); - resolve(); - } - ); - } - ); - itAsync( - "throws an error if only errors are returned", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 400, text: textWithErrors }) - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("should not have called result because we have no data"); - }, - (e) => { - expect(e.message).toMatch(/Received status code 400/); - expect(e.statusCode).toBe(400); - expect(e.result).toEqual(responseBody); - resolve(); - } - ); - } - ); - itAsync( - "throws an error if empty response from the server ", - (resolve, reject) => { - fetch.mockReturnValueOnce(Promise.resolve({ text })); - text.mockReturnValueOnce(Promise.resolve('{ "body": "boo" }')); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const result = await stream.takeNext(); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: Error) => { - expect(e.message).toMatch( - /Server response was missing for query 'SampleQuery'/ - ); - }) - ); - } - ); - itAsync("throws if the body can't be stringified", (resolve, reject) => { + expect(result).toEqual(responseBody); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 400/); + expect(error.statusCode).toBe(400); + expect(error.result).toEqual(responseBody); + }); + + it("throws an error if only errors are returned", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 400, text: textWithErrors }) + ); + + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 400/); + expect(error.statusCode).toBe(400); + expect(error.result).toEqual(responseBody); + }); + + it("throws an error if empty response from the server ", async () => { + fetch.mockReturnValueOnce(Promise.resolve({ text })); + text.mockReturnValueOnce(Promise.resolve('{ "body": "boo" }')); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch( + /Server response was missing for query 'SampleQuery'/ + ); + }); + + it("throws if the body can't be stringified", async () => { fetch.mockReturnValueOnce(Promise.resolve({ data: {}, text })); const link = createHttpLink({ uri: "data", @@ -1408,16 +1281,14 @@ describe("HttpLink", () => { a, b, }; - execute(link, { query: sampleQuery, variables }).subscribe( - (result) => { - reject("next should have been thrown from the link"); - }, - makeCallback(resolve, reject, (e: ClientParseError) => { - expect(e.message).toMatch(/Payload is not serializable/); - expect(e.parseError.message).toMatch( - /Converting circular structure to JSON/ - ); - }) + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + const error: ClientParseError = await stream.takeError(); + + expect(error.message).toMatch(/Payload is not serializable/); + expect(error.parseError.message).toMatch( + /Converting circular structure to JSON/ ); }); @@ -1563,51 +1434,41 @@ describe("HttpLink", () => { const body = "{"; const unparsableJson = jest.fn(() => Promise.resolve(body)); - itAsync( - "throws a Server error if response is > 300 with unparsable json", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 400, text: unparsableJson }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + it("throws a Server error if response is > 300 with unparsable json", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 400, text: unparsableJson }) + ); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerParseError) => { - expect(e.message).toMatch( - "Response not successful: Received status code 400" - ); - expect(e.statusCode).toBe(400); - expect(e.response).toBeDefined(); - expect(e.bodyText).toBe(undefined); - }) - ); - } - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); - itAsync( - "throws a ServerParse error if response is 200 with unparsable json", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 200, text: unparsableJson }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const error: ServerParseError = await stream.takeError(); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerParseError) => { - expect(e.message).toMatch(/JSON/); - expect(e.statusCode).toBe(200); - expect(e.response).toBeDefined(); - expect(e.bodyText).toBe(body); - }) - ); - } - ); + expect(error.message).toMatch( + "Response not successful: Received status code 400" + ); + expect(error.statusCode).toBe(400); + expect(error.response).toBeDefined(); + expect(error.bodyText).toBe(undefined); + }); + + it("throws a ServerParse error if response is 200 with unparsable json", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 200, text: unparsableJson }) + ); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error: ServerParseError = await stream.takeError(); + + expect(error.message).toMatch(/JSON/); + expect(error.statusCode).toBe(200); + expect(error.response).toBeDefined(); + expect(error.bodyText).toBe(body); + }); }); describe("Multipart responses", () => { @@ -1854,72 +1715,63 @@ describe("HttpLink", () => { ); }); - itAsync( - "sets correct accept header on request with deferred query", - (resolve, reject) => { - const stream = Readable.from( - body.split("\r\n").map((line) => line + "\r\n") - ); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ "Content-Type": "multipart/mixed" }), - })); - const link = new HttpLink({ - fetch: fetch as any, - }); - execute(link, { - query: sampleDeferredQuery, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetch).toHaveBeenCalledWith( - "/graphql", - expect.objectContaining({ - headers: { - "content-type": "application/json", - accept: - "multipart/mixed;deferSpec=20220824,application/json", - }, - }) - ); - }) - ); - } - ); + it("sets correct accept header on request with deferred query", async () => { + const stream = Readable.from( + body.split("\r\n").map((line) => line + "\r\n") + ); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ "Content-Type": "multipart/mixed" }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); + const observable = execute(link, { query: sampleDeferredQuery }); + const observableStream = new ObservableStream(observable); + + await expect(observableStream).toEmitNext(); + + expect(fetch).toHaveBeenCalledWith( + "/graphql", + expect.objectContaining({ + headers: { + "content-type": "application/json", + accept: "multipart/mixed;deferSpec=20220824,application/json", + }, + }) + ); + }); // ensure that custom directives beginning with '@defer..' do not trigger // custom accept header for multipart responses - itAsync( - "sets does not set accept header on query with custom directive begging with @defer", - (resolve, reject) => { - const stream = Readable.from( - body.split("\r\n").map((line) => line + "\r\n") - ); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ "Content-Type": "multipart/mixed" }), - })); - const link = new HttpLink({ - fetch: fetch as any, - }); - execute(link, { - query: sampleQueryCustomDirective, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetch).toHaveBeenCalledWith( - "/graphql", - expect.objectContaining({ - headers: { - accept: "*/*", - "content-type": "application/json", - }, - }) - ); - }) - ); - } - ); + it("sets does not set accept header on query with custom directive begging with @defer", async () => { + const stream = Readable.from( + body.split("\r\n").map((line) => line + "\r\n") + ); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ "Content-Type": "multipart/mixed" }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); + const observable = execute(link, { query: sampleQueryCustomDirective }); + const observableStream = new ObservableStream(observable); + + await expect(observableStream).toEmitNext(); + + expect(fetch).toHaveBeenCalledWith( + "/graphql", + expect.objectContaining({ + headers: { + accept: "*/*", + "content-type": "application/json", + }, + }) + ); + }); }); describe("subscriptions", () => { @@ -2194,38 +2046,34 @@ describe("HttpLink", () => { ); }); - itAsync( - "sets correct accept header on request with subscription", - (resolve, reject) => { - const stream = Readable.from( - subscriptionsBody.split("\r\n").map((line) => line + "\r\n") - ); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ "Content-Type": "multipart/mixed" }), - })); - const link = new HttpLink({ - fetch: fetch as any, - }); - execute(link, { - query: sampleSubscription, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetch).toHaveBeenCalledWith( - "/graphql", - expect.objectContaining({ - headers: { - "content-type": "application/json", - accept: - "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", - }, - }) - ); - }) - ); - } - ); + it("sets correct accept header on request with subscription", async () => { + const stream = Readable.from( + subscriptionsBody.split("\r\n").map((line) => line + "\r\n") + ); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ "Content-Type": "multipart/mixed" }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); + const observable = execute(link, { query: sampleSubscription }); + const observableStream = new ObservableStream(observable); + + await expect(observableStream).toEmitNext(); + + expect(fetch).toHaveBeenCalledWith( + "/graphql", + expect.objectContaining({ + headers: { + "content-type": "application/json", + accept: + "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", + }, + }) + ); + }); }); }); }); diff --git a/src/link/persisted-queries/__tests__/persisted-queries.test.ts b/src/link/persisted-queries/__tests__/persisted-queries.test.ts index 7b4fecaf99..84ce1840b8 100644 --- a/src/link/persisted-queries/__tests__/persisted-queries.test.ts +++ b/src/link/persisted-queries/__tests__/persisted-queries.test.ts @@ -9,8 +9,9 @@ import { Observable } from "../../../utilities"; import { createHttpLink } from "../../http/createHttpLink"; import { createPersistedQueryLink as createPersistedQuery, VERSION } from ".."; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; import { toPromise } from "../../utils"; +import { ObservableStream } from "../../../testing/internal"; // Necessary configuration in order to mock multiple requests // to a single (/graphql) endpoint @@ -79,52 +80,53 @@ describe("happy path", () => { fetchMock.restore(); }); - itAsync( - "sends a sha256 hash of the query under extensions", - (resolve, reject) => { - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })) - ); - const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - expect(request!.body!).toBe( - JSON.stringify({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, - }, - }) - ); - resolve(); - }, reject); - } - ); + it("sends a sha256 hash of the query under extensions", async () => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })) + ); + const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; - itAsync("sends a version along with the request", (resolve, reject) => { + expect(uri).toEqual("/graphql"); + expect(request!.body!).toBe( + JSON.stringify({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, + }, + }, + }) + ); + }); + + it("sends a version along with the request", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })) ); const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - const parsed = JSON.parse(request!.body!.toString()); - expect(parsed.extensions.persistedQuery.version).toBe(VERSION); - resolve(); - }, reject); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; + expect(uri).toEqual("/graphql"); + + const parsed = JSON.parse(request!.body!.toString()); + expect(parsed.extensions.persistedQuery.version).toBe(VERSION); }); - itAsync("memoizes between requests", (resolve, reject) => { + it("memoizes between requests", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })), @@ -140,15 +142,23 @@ describe("happy path", () => { createHttpLink() ); - execute(link, { query, variables }).subscribe((result) => { + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + await expect(stream).toComplete(); + expect(hashSpy).toHaveBeenCalledTimes(1); + } + + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + await expect(stream).toComplete(); expect(hashSpy).toHaveBeenCalledTimes(1); - expect(result.data).toEqual(data); - execute(link, { query, variables }).subscribe((result2) => { - expect(hashSpy).toHaveBeenCalledTimes(1); - expect(result2.data).toEqual(data); - resolve(); - }, reject); - }, reject); + } }); it("clears the cache when calling `resetHashCache`", async () => { @@ -177,7 +187,7 @@ describe("happy path", () => { await expect(hashRefs[0]).toBeGarbageCollected(); }); - itAsync("supports loading the hash from other method", (resolve, reject) => { + it("supports loading the hash from other method", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })) @@ -187,33 +197,34 @@ describe("happy path", () => { createHttpLink() ); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - const parsed = JSON.parse(request!.body!.toString()); - expect(parsed.extensions.persistedQuery.sha256Hash).toBe("foo"); - resolve(); - }, reject); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; + expect(uri).toEqual("/graphql"); + + const parsed = JSON.parse(request!.body!.toString()); + expect(parsed.extensions.persistedQuery.sha256Hash).toBe("foo"); }); - itAsync("errors if unable to convert to sha256", (resolve, reject) => { + it("errors if unable to convert to sha256", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })) ); const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query: "1234", variables } as any).subscribe( - reject as any, - (error) => { - expect(error.message).toMatch(/Invalid AST Node/); - resolve(); - } - ); + const observable = execute(link, { query: "1234", variables } as any); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Invalid AST Node/); }); - itAsync("unsubscribes correctly", (resolve, reject) => { + it("unsubscribes correctly", async () => { const delay = new ApolloLink(() => { return new Observable((ob) => { setTimeout(() => { @@ -224,92 +235,70 @@ describe("happy path", () => { }); const link = createPersistedQuery({ sha256 }).concat(delay); - const sub = execute(link, { query, variables }).subscribe( - reject, - reject, - reject - ); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); - setTimeout(() => { - sub.unsubscribe(); - resolve(); - }, 10); + await wait(10); + + stream.unsubscribe(); + + await expect(stream).not.toEmitAnything({ timeout: 150 }); }); - itAsync( - "should error if `sha256` and `generateHash` options are both missing", - (resolve, reject) => { - const createPersistedQueryFn = createPersistedQuery as any; - try { - createPersistedQueryFn(); - reject("should have thrown an error"); - } catch (error) { - expect( - (error as Error).message.indexOf( - 'Missing/invalid "sha256" or "generateHash" function' - ) - ).toBe(0); - resolve(); - } - } - ); + it("should error if `sha256` and `generateHash` options are both missing", async () => { + const createPersistedQueryFn = createPersistedQuery as any; + + expect(() => createPersistedQueryFn()).toThrow( + 'Missing/invalid "sha256" or "generateHash" function' + ); + }); - itAsync( - "should error if `sha256` or `generateHash` options are not functions", - (resolve, reject) => { + it.each(["sha256", "generateHash"])( + "should error if `%s` option is not a function", + async (option) => { const createPersistedQueryFn = createPersistedQuery as any; - [{ sha256: "ooops" }, { generateHash: "ooops" }].forEach((options) => { - try { - createPersistedQueryFn(options); - reject("should have thrown an error"); - } catch (error) { - expect( - (error as Error).message.indexOf( - 'Missing/invalid "sha256" or "generateHash" function' - ) - ).toBe(0); - resolve(); - } - }); + + expect(() => createPersistedQueryFn({ [option]: "ooops" })).toThrow( + 'Missing/invalid "sha256" or "generateHash" function' + ); } ); - itAsync( - "should work with a synchronous SHA-256 function", - (resolve, reject) => { - const crypto = require("crypto"); - const sha256Hash = crypto.createHmac("sha256", queryString).digest("hex"); + it("should work with a synchronous SHA-256 function", async () => { + const crypto = require("crypto"); + const sha256Hash = crypto.createHmac("sha256", queryString).digest("hex"); - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })) - ); - const link = createPersistedQuery({ - sha256(data) { - return crypto.createHmac("sha256", data).digest("hex"); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })) + ); + const link = createPersistedQuery({ + sha256(data) { + return crypto.createHmac("sha256", data).digest("hex"); + }, + }).concat(createHttpLink()); + + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; + + expect(uri).toEqual("/graphql"); + expect(request!.body!).toBe( + JSON.stringify({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: sha256Hash, + }, }, - }).concat(createHttpLink()); - - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - expect(request!.body!).toBe( - JSON.stringify({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: sha256Hash, - }, - }, - }) - ); - resolve(); - }, reject); - } - ); + }) + ); + }); }); describe("failure path", () => { @@ -356,98 +345,99 @@ describe("failure path", () => { }) ); - itAsync( - "sends GET for the first response only with useGETForHashedQueries", - (resolve, reject) => { - const params = new URLSearchParams({ - operationName: "Test", - variables: JSON.stringify({ - id: 1, - }), - extensions: JSON.stringify({ - persistedQuery: { - version: 1, - sha256Hash: hash, - }, - }), - }).toString(); - fetchMock.get( - `/graphql?${params}`, - () => new Promise((resolve) => resolve({ body: errorResponse })) - ); - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })) - ); - const link = createPersistedQuery({ - sha256, - useGETForHashedQueries: true, - }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(failure!.method).toBe("GET"); - expect(failure!.body).not.toBeDefined(); - const [, [, success]] = fetchMock.calls(); - expect(success!.method).toBe("POST"); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - resolve(); - }, reject); - } - ); + it("sends GET for the first response only with useGETForHashedQueries", async () => { + const params = new URLSearchParams({ + operationName: "Test", + variables: JSON.stringify({ + id: 1, + }), + extensions: JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: hash, + }, + }), + }).toString(); + fetchMock.get( + `/graphql?${params}`, + () => new Promise((resolve) => resolve({ body: errorResponse })) + ); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })) + ); + const link = createPersistedQuery({ + sha256, + useGETForHashedQueries: true, + }).concat(createHttpLink()); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); - itAsync( - "sends POST for both requests without useGETForHashedQueries", - (resolve, reject) => { - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: errorResponse })), - { repeat: 1 } - ); - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 1 } - ); - const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(failure!.method).toBe("POST"); - expect(JSON.parse(failure!.body!.toString())).toEqual({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, - }, - }); - const [, [, success]] = fetchMock.calls(); - expect(success!.method).toBe("POST"); - expect(JSON.parse(success!.body!.toString())).toEqual({ - operationName: "Test", - query: queryString, - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, - }, - }); - resolve(); - }, reject); - } - ); + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(failure!.method).toBe("GET"); + expect(failure!.body).not.toBeDefined(); + + const [, [, success]] = fetchMock.calls(); + + expect(success!.method).toBe("POST"); + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery.sha256Hash + ).toBe(hash); + }); + + it("sends POST for both requests without useGETForHashedQueries", async () => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: errorResponse })), + { repeat: 1 } + ); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); + const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(failure!.method).toBe("POST"); + expect(JSON.parse(failure!.body!.toString())).toEqual({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, + }, + }, + }); + + const [, [, success]] = fetchMock.calls(); + + expect(success!.method).toBe("POST"); + expect(JSON.parse(success!.body!.toString())).toEqual({ + operationName: "Test", + query: queryString, + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, + }, + }, + }); + }); // https://github.com/apollographql/apollo-client/pull/7456 - itAsync("forces POST request when sending full query", (resolve, reject) => { + it("forces POST request when sending full query", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: giveUpResponse })), @@ -469,29 +459,33 @@ describe("failure path", () => { return true; }, }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(failure!.method).toBe("POST"); - expect(JSON.parse(failure!.body!.toString())).toEqual({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(failure!.method).toBe("POST"); + expect(JSON.parse(failure!.body!.toString())).toEqual({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, }, - }); - const [, [, success]] = fetchMock.calls(); - expect(success!.method).toBe("POST"); - expect(JSON.parse(success!.body!.toString())).toEqual({ - operationName: "Test", - query: queryString, - variables, - }); - resolve(); - }, reject); + }, + }); + + const [, [, success]] = fetchMock.calls(); + + expect(success!.method).toBe("POST"); + expect(JSON.parse(success!.body!.toString())).toEqual({ + operationName: "Test", + query: queryString, + variables, + }); }); it.each([ @@ -583,7 +577,7 @@ describe("failure path", () => { } ); - itAsync("works with multiple errors", (resolve, reject) => { + it("works with multiple errors", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: multiResponse })), @@ -595,76 +589,84 @@ describe("failure path", () => { { repeat: 1 } ); const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(JSON.parse(failure!.body!.toString()).query).not.toBeDefined(); - const [, [, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - resolve(); - }, reject); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(JSON.parse(failure!.body!.toString()).query).not.toBeDefined(); + + const [, [, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery.sha256Hash + ).toBe(hash); }); describe.each([[400], [500]])("status %s", (status) => { - itAsync( - `handles a ${status} network with a "PERSISTED_QUERY_NOT_FOUND" error and still retries`, - (resolve, reject) => { - let requestCount = 0; - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 1 } - ); + it(`handles a ${status} network with a "PERSISTED_QUERY_NOT_FOUND" error and still retries`, async () => { + let requestCount = 0; + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); - // mock it again so we can verify it doesn't try anymore - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 5 } - ); + // mock it again so we can verify it doesn't try anymore + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 5 } + ); - const fetcher = (...args: any[]) => { - if (++requestCount % 2) { - return Promise.resolve({ - json: () => Promise.resolve(errorResponseWithCode), - text: () => Promise.resolve(errorResponseWithCode), - status, - }); - } - // @ts-expect-error - return global.fetch.apply(null, args); - }; - const link = createPersistedQuery({ sha256 }).concat( - createHttpLink({ fetch: fetcher } as any) - ); + const fetcher = (...args: any[]) => { + if (++requestCount % 2) { + return Promise.resolve({ + json: () => Promise.resolve(errorResponseWithCode), + text: () => Promise.resolve(errorResponseWithCode), + status, + }); + } + // @ts-expect-error + return global.fetch.apply(null, args); + }; + const link = createPersistedQuery({ sha256 }).concat( + createHttpLink({ fetch: fetcher } as any) + ); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - execute(link, { query, variables }).subscribe((secondResult) => { - expect(secondResult.data).toEqual(data); - const [, [, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe( - queryString - ); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - resolve(); - }, reject); - }, reject); + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery + .sha256Hash + ).toBe(hash); } - ); + + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [, [, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery + .sha256Hash + ).toBe(hash); + } + }); it(`will fail on an unrelated ${status} network error, but still send a hash the next request`, async () => { let failed = false; @@ -711,43 +713,42 @@ describe("failure path", () => { ).toBe(hash); }); - itAsync( - `handles ${status} response network error and graphql error without disabling persistedQuery support`, - (resolve, reject) => { - let failed = false; - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 1 } - ); + it(`handles ${status} response network error and graphql error without disabling persistedQuery support`, async () => { + let failed = false; + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); - const fetcher = (...args: any[]) => { - if (!failed) { - failed = true; - return Promise.resolve({ - json: () => Promise.resolve(errorResponse), - text: () => Promise.resolve(errorResponse), - status, - }); - } - // @ts-expect-error - return global.fetch.apply(null, args); - }; - - const link = createPersistedQuery({ sha256 }).concat( - createHttpLink({ fetch: fetcher } as any) - ); + const fetcher = (...args: any[]) => { + if (!failed) { + failed = true; + return Promise.resolve({ + json: () => Promise.resolve(errorResponse), + text: () => Promise.resolve(errorResponse), + status, + }); + } + // @ts-expect-error + return global.fetch.apply(null, args); + }; - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions - ).not.toBeUndefined(); - resolve(); - }, reject); - } - ); + const link = createPersistedQuery({ sha256 }).concat( + createHttpLink({ fetch: fetcher } as any) + ); + + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions + ).not.toBeUndefined(); + }); }); }); diff --git a/src/link/ws/__tests__/webSocketLink.ts b/src/link/ws/__tests__/webSocketLink.ts index d24b118c44..5859d6ed78 100644 --- a/src/link/ws/__tests__/webSocketLink.ts +++ b/src/link/ws/__tests__/webSocketLink.ts @@ -5,7 +5,7 @@ import gql from "graphql-tag"; import { Observable } from "../../../utilities"; import { execute } from "../../core"; import { WebSocketLink } from ".."; -import { itAsync } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const query = gql` query SampleQuery { @@ -43,95 +43,84 @@ describe("WebSocketLink", () => { // it('should pass the correct initialization parameters to the Subscription Client', () => { // }); - itAsync( - "should call request on the client for a query", - (resolve, reject) => { - const result = { data: { data: "result" } }; - const client: any = {}; - const observable = Observable.of(result); - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(); - client.request.mockReturnValueOnce(observable); - const link = new WebSocketLink(client); - - const obs = execute(link, { query }); - expect(obs).toEqual(observable); - obs.subscribe((data) => { - expect(data).toEqual(result); - expect(client.request).toHaveBeenCalledTimes(1); - resolve(); - }); - } - ); - - itAsync( - "should call query on the client for a mutation", - (resolve, reject) => { - const result = { data: { data: "result" } }; - const client: any = {}; - const observable = Observable.of(result); - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(); - client.request.mockReturnValueOnce(observable); - const link = new WebSocketLink(client); - - const obs = execute(link, { query: mutation }); - expect(obs).toEqual(observable); - obs.subscribe((data) => { - expect(data).toEqual(result); - expect(client.request).toHaveBeenCalledTimes(1); - resolve(); - }); - } - ); - - itAsync( - "should call request on the subscriptions client for subscription", - (resolve, reject) => { - const result = { data: { data: "result" } }; - const client: any = {}; - const observable = Observable.of(result); - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(); - client.request.mockReturnValueOnce(observable); - const link = new WebSocketLink(client); - - const obs = execute(link, { query: subscription }); - expect(obs).toEqual(observable); - obs.subscribe((data) => { - expect(data).toEqual(result); - expect(client.request).toHaveBeenCalledTimes(1); - resolve(); - }); - } - ); - - itAsync( - "should call next with multiple results for subscription", - (resolve, reject) => { - const results = [ - { data: { data: "result1" } }, - { data: { data: "result2" } }, - ]; - const client: any = {}; - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(() => { - const copy = [...results]; - return new Observable((observer) => { - observer.next(copy[0]); - observer.next(copy[1]); - }); - }); + it("should call request on the client for a query", async () => { + const result = { data: { data: "result" } }; + const client: any = {}; + const observable = Observable.of(result); + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(); + client.request.mockReturnValueOnce(observable); + const link = new WebSocketLink(client); + + const obs = execute(link, { query }); + expect(obs).toEqual(observable); + + const stream = new ObservableStream(obs); + + await expect(stream).toEmitValue(result); + expect(client.request).toHaveBeenCalledTimes(1); + }); + + it("should call query on the client for a mutation", async () => { + const result = { data: { data: "result" } }; + const client: any = {}; + const observable = Observable.of(result); + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(); + client.request.mockReturnValueOnce(observable); + const link = new WebSocketLink(client); + + const obs = execute(link, { query: mutation }); + expect(obs).toEqual(observable); - const link = new WebSocketLink(client); + const stream = new ObservableStream(obs); - execute(link, { query: subscription }).subscribe((data) => { - expect(client.request).toHaveBeenCalledTimes(1); - expect(data).toEqual(results.shift()); - if (results.length === 0) { - resolve(); - } + await expect(stream).toEmitValue(result); + expect(client.request).toHaveBeenCalledTimes(1); + }); + + it("should call request on the subscriptions client for subscription", async () => { + const result = { data: { data: "result" } }; + const client: any = {}; + const observable = Observable.of(result); + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(); + client.request.mockReturnValueOnce(observable); + const link = new WebSocketLink(client); + + const obs = execute(link, { query: subscription }); + expect(obs).toEqual(observable); + + const stream = new ObservableStream(obs); + + await expect(stream).toEmitValue(result); + expect(client.request).toHaveBeenCalledTimes(1); + }); + + it("should call next with multiple results for subscription", async () => { + const results = [ + { data: { data: "result1" } }, + { data: { data: "result2" } }, + ]; + const client: any = {}; + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(() => { + const copy = [...results]; + return new Observable((observer) => { + observer.next(copy[0]); + observer.next(copy[1]); }); - } - ); + }); + + const link = new WebSocketLink(client); + + const observable = execute(link, { query: subscription }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(results.shift()); + await expect(stream).toEmitValue(results.shift()); + + expect(client.request).toHaveBeenCalledTimes(1); + expect(results).toHaveLength(0); + }); }); diff --git a/src/react/context/__tests__/ApolloConsumer.test.tsx b/src/react/context/__tests__/ApolloConsumer.test.tsx index aed5384c41..a27a02d782 100644 --- a/src/react/context/__tests__/ApolloConsumer.test.tsx +++ b/src/react/context/__tests__/ApolloConsumer.test.tsx @@ -7,7 +7,6 @@ import { InMemoryCache as Cache } from "../../../cache"; import { ApolloProvider } from "../ApolloProvider"; import { ApolloConsumer } from "../ApolloConsumer"; import { getApolloContext } from "../ApolloContext"; -import { itAsync } from "../../../testing"; const client = new ApolloClient({ cache: new Cache(), @@ -15,17 +14,13 @@ const client = new ApolloClient({ }); describe(" component", () => { - itAsync("has a render prop", (resolve, reject) => { + it("has a render prop", (done) => { render( {(clientRender) => { - try { - expect(clientRender).toBe(client); - resolve(); - } catch (e) { - reject(e); - } + expect(clientRender).toBe(client); + done(); return null; }} diff --git a/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx b/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx index 70daf89795..dc250d8f56 100644 --- a/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx +++ b/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx @@ -8,7 +8,7 @@ import { DocumentNode } from "graphql"; import { ApolloClient, TypedDocumentNode } from "../../../../core"; import { ApolloProvider } from "../../../context"; import { InMemoryCache as Cache } from "../../../../cache"; -import { itAsync, mockSingleLink } from "../../../../testing"; +import { mockSingleLink } from "../../../../testing"; import { Query } from "../../../components"; import { getDataFromTree, getMarkupFromTree } from "../../../ssr"; import { graphql } from "../../graphql"; @@ -543,86 +543,78 @@ describe("SSR", () => { }); }); - itAsync( - "should allow for setting state in a component", - (resolve, reject) => { - const query = gql` - query user($id: ID) { - currentUser(id: $id) { - firstName - } + it("should allow for setting state in a component", async () => { + const query = gql` + query user($id: ID) { + currentUser(id: $id) { + firstName } - `; - const resultData = { currentUser: { firstName: "James" } }; - const variables = { id: "1" }; - const link = mockSingleLink({ - request: { query, variables }, - result: { data: resultData }, - }); - - const cache = new Cache({ addTypename: false }); - const apolloClient = new ApolloClient({ - link, - cache, - }); - - interface Props { - id: string; } - interface Data { - currentUser: { - firstName: string; + `; + const resultData = { currentUser: { firstName: "James" } }; + const variables = { id: "1" }; + const link = mockSingleLink({ + request: { query, variables }, + result: { data: resultData }, + }); + + const cache = new Cache({ addTypename: false }); + const apolloClient = new ApolloClient({ + link, + cache, + }); + + interface Props { + id: string; + } + interface Data { + currentUser: { + firstName: string; + }; + } + interface Variables { + id: string; + } + + class Element extends React.Component< + ChildProps, + { thing: number } + > { + state = { thing: 1 }; + + static getDerivedStateFromProps() { + return { + thing: 2, }; } - interface Variables { - id: string; - } - class Element extends React.Component< - ChildProps, - { thing: number } - > { - state = { thing: 1 }; + render() { + const { data } = this.props; + expect(this.state.thing).toBe(2); + return ( +
+ {!data || data.loading || !data.currentUser ? + "loading" + : data.currentUser.firstName} +
+ ); + } + } - static getDerivedStateFromProps() { - return { - thing: 2, - }; - } + const ElementWithData = graphql(query)(Element); - render() { - const { data } = this.props; - expect(this.state.thing).toBe(2); - return ( -
- {!data || data.loading || !data.currentUser ? - "loading" - : data.currentUser.firstName} -
- ); - } - } + const app = ( + + + + ); - const ElementWithData = graphql(query)(Element); + await getDataFromTree(app); - const app = ( - - - - ); - - getDataFromTree(app) - .then(() => { - const initialState = cache.extract(); - expect(initialState).toBeTruthy(); - expect( - initialState.ROOT_QUERY!['currentUser({"id":"1"})'] - ).toBeTruthy(); - resolve(); - }) - .catch(console.error); - } - ); + const initialState = cache.extract(); + expect(initialState).toBeTruthy(); + expect(initialState.ROOT_QUERY!['currentUser({"id":"1"})']).toBeTruthy(); + }); it("should correctly initialize an empty state to null", () => { class Element extends React.Component { @@ -651,7 +643,7 @@ describe("SSR", () => { return getDataFromTree(); }); - itAsync("should allow prepping state from props", (resolve, reject) => { + it("should allow prepping state from props", async () => { const query = gql` query user($id: ID) { currentUser(id: $id) { @@ -730,16 +722,11 @@ describe("SSR", () => {
); - getDataFromTree(app) - .then(() => { - const initialState = apolloClient.cache.extract(); - expect(initialState).toBeTruthy(); - expect( - initialState.ROOT_QUERY!['currentUser({"id":"1"})'] - ).toBeTruthy(); - resolve(); - }) - .catch(console.error); + await getDataFromTree(app); + + const initialState = apolloClient.cache.extract(); + expect(initialState).toBeTruthy(); + expect(initialState.ROOT_QUERY!['currentUser({"id":"1"})']).toBeTruthy(); }); it("shouldn't run queries if ssr is turned to off", () => { diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx index 8e81130201..2ac84fb45b 100644 --- a/src/react/hooks/__tests__/useMutation.test.tsx +++ b/src/react/hooks/__tests__/useMutation.test.tsx @@ -19,7 +19,6 @@ import { } from "../../../core"; import { InMemoryCache } from "../../../cache"; import { - itAsync, MockedProvider, MockSubscriptionLink, mockSingleLink, @@ -1500,7 +1499,7 @@ describe("useMutation Hook", () => { await waitFor(() => expect(variablesMatched).toBe(true)); }); - itAsync("should be called with the provided context", (resolve, reject) => { + it("should be called with the provided context", async () => { const context = { id: 3 }; const variables = { @@ -1544,13 +1543,13 @@ describe("useMutation Hook", () => { ); - return waitFor(() => { + await waitFor(() => { expect(foundContext).toBe(true); - }).then(resolve, reject); + }); }); describe("If context is not provided", () => { - itAsync("should be undefined", (resolve, reject) => { + it("should be undefined", async () => { const variables = { description: "Get milk!", }; @@ -1587,92 +1586,89 @@ describe("useMutation Hook", () => { ); - return waitFor(() => { + await waitFor(() => { expect(checkedContext).toBe(true); - }).then(resolve, reject); + }); }); }); }); describe("Optimistic response", () => { - itAsync( - "should support optimistic response handling", - async (resolve, reject) => { - const optimisticResponse = { - __typename: "Mutation", - createTodo: { - id: 1, - description: "TEMPORARY", - priority: "High", - __typename: "Todo", - }, - }; + it("should support optimistic response handling", async () => { + const optimisticResponse = { + __typename: "Mutation", + createTodo: { + id: 1, + description: "TEMPORARY", + priority: "High", + __typename: "Todo", + }, + }; - const variables = { - description: "Get milk!", - }; + const variables = { + description: "Get milk!", + }; - const mocks = [ - { - request: { - query: CREATE_TODO_MUTATION, - variables, - }, - result: { data: CREATE_TODO_RESULT }, + const mocks = [ + { + request: { + query: CREATE_TODO_MUTATION, + variables, }, - ]; + result: { data: CREATE_TODO_RESULT }, + }, + ]; - const link = mockSingleLink(...mocks).setOnError(reject); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - }); + const link = mockSingleLink(...mocks); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + }); - let renderCount = 0; - const Component = () => { - const [createTodo, { loading, data }] = useMutation( - CREATE_TODO_MUTATION, - { optimisticResponse } - ); + let renderCount = 0; + const Component = () => { + const [createTodo, { loading, data }] = useMutation( + CREATE_TODO_MUTATION, + { optimisticResponse } + ); - switch (renderCount) { - case 0: - expect(loading).toBeFalsy(); - expect(data).toBeUndefined(); - void createTodo({ variables }); - - const dataInStore = client.cache.extract(true); - expect(dataInStore["Todo:1"]).toEqual( - optimisticResponse.createTodo - ); - - break; - case 1: - expect(loading).toBeTruthy(); - expect(data).toBeUndefined(); - break; - case 2: - expect(loading).toBeFalsy(); - expect(data).toEqual(CREATE_TODO_RESULT); - break; - default: - } - renderCount += 1; - return null; - }; + switch (renderCount) { + case 0: + expect(loading).toBeFalsy(); + expect(data).toBeUndefined(); + void createTodo({ variables }); - render( - - - - ); + const dataInStore = client.cache.extract(true); + expect(dataInStore["Todo:1"]).toEqual( + optimisticResponse.createTodo + ); + + break; + case 1: + expect(loading).toBeTruthy(); + expect(data).toBeUndefined(); + break; + case 2: + expect(loading).toBeFalsy(); + expect(data).toEqual(CREATE_TODO_RESULT); + break; + default: + } + renderCount += 1; + return null; + }; - return waitFor(() => { - expect(renderCount).toBe(3); - }).then(resolve, reject); - } - ); + render( + + + + ); + + await waitFor(() => { + expect(renderCount).toBe(3); + }); + }); it("should be called with the provided context", async () => { const optimisticResponse = { diff --git a/src/react/hooks/__tests__/useReactiveVar.test.tsx b/src/react/hooks/__tests__/useReactiveVar.test.tsx index c78d7e6ec8..84cb17cb30 100644 --- a/src/react/hooks/__tests__/useReactiveVar.test.tsx +++ b/src/react/hooks/__tests__/useReactiveVar.test.tsx @@ -1,7 +1,6 @@ import React, { StrictMode, useEffect } from "react"; import { screen, render, waitFor, act } from "@testing-library/react"; -import { itAsync } from "../../../testing"; import { makeVar } from "../../../core"; import { useReactiveVar } from "../useReactiveVar"; @@ -47,92 +46,87 @@ describe("useReactiveVar Hook", () => { }); }); - itAsync( - "works when two components share a variable", - async (resolve, reject) => { - const counterVar = makeVar(0); - - let parentRenderCount = 0; - function Parent() { - const count = useReactiveVar(counterVar); + it("works when two components share a variable", async () => { + const counterVar = makeVar(0); - switch (++parentRenderCount) { - case 1: - expect(count).toBe(0); - break; - case 2: - expect(count).toBe(1); - break; - case 3: - expect(count).toBe(11); - break; - default: - reject(`too many (${parentRenderCount}) parent renders`); - } + let parentRenderCount = 0; + function Parent() { + const count = useReactiveVar(counterVar); - return ; + switch (++parentRenderCount) { + case 1: + expect(count).toBe(0); + break; + case 2: + expect(count).toBe(1); + break; + case 3: + expect(count).toBe(11); + break; + default: + throw new Error(`too many (${parentRenderCount}) parent renders`); } - let childRenderCount = 0; - function Child() { - const count = useReactiveVar(counterVar); + return ; + } - switch (++childRenderCount) { - case 1: - expect(count).toBe(0); - break; - case 2: - expect(count).toBe(1); - break; - case 3: - expect(count).toBe(11); - break; - default: - reject(`too many (${childRenderCount}) child renders`); - } + let childRenderCount = 0; + function Child() { + const count = useReactiveVar(counterVar); - return null; + switch (++childRenderCount) { + case 1: + expect(count).toBe(0); + break; + case 2: + expect(count).toBe(1); + break; + case 3: + expect(count).toBe(11); + break; + default: + throw new Error(`too many (${childRenderCount}) child renders`); } - render(); + return null; + } - await waitFor(() => { - expect(parentRenderCount).toBe(1); - }); + render(); - await waitFor(() => { - expect(childRenderCount).toBe(1); - }); + await waitFor(() => { + expect(parentRenderCount).toBe(1); + }); - expect(counterVar()).toBe(0); - act(() => { - counterVar(1); - }); + await waitFor(() => { + expect(childRenderCount).toBe(1); + }); - await waitFor(() => { - expect(parentRenderCount).toBe(2); - }); - await waitFor(() => { - expect(childRenderCount).toBe(2); - }); + expect(counterVar()).toBe(0); + act(() => { + counterVar(1); + }); - expect(counterVar()).toBe(1); - act(() => { - counterVar(counterVar() + 10); - }); + await waitFor(() => { + expect(parentRenderCount).toBe(2); + }); + await waitFor(() => { + expect(childRenderCount).toBe(2); + }); - await waitFor(() => { - expect(parentRenderCount).toBe(3); - }); - await waitFor(() => { - expect(childRenderCount).toBe(3); - }); + expect(counterVar()).toBe(1); + act(() => { + counterVar(counterVar() + 10); + }); - expect(counterVar()).toBe(11); + await waitFor(() => { + expect(parentRenderCount).toBe(3); + }); + await waitFor(() => { + expect(childRenderCount).toBe(3); + }); - resolve(); - } - ); + expect(counterVar()).toBe(11); + }); it("does not update if component has been unmounted", async () => { const counterVar = makeVar(0); @@ -252,7 +246,7 @@ describe("useReactiveVar Hook", () => { }); }); - itAsync("works with strict mode", async (resolve, reject) => { + it("works with strict mode", async () => { const counterVar = makeVar(0); const mock = jest.fn(); @@ -289,94 +283,84 @@ describe("useReactiveVar Hook", () => { expect(mock).toHaveBeenNthCalledWith(2, 1); } }); - - resolve(); }); - itAsync( - "works with multiple synchronous calls", - async (resolve, reject) => { - const counterVar = makeVar(0); - function Component() { - const count = useReactiveVar(counterVar); + it("works with multiple synchronous calls", async () => { + const counterVar = makeVar(0); + function Component() { + const count = useReactiveVar(counterVar); - return
{count}
; - } + return
{count}
; + } - render(); - void Promise.resolve().then(() => { - counterVar(1); - counterVar(2); - counterVar(3); - counterVar(4); - counterVar(5); - counterVar(6); - counterVar(7); - counterVar(8); - counterVar(9); - counterVar(10); - }); - - await waitFor(() => { - expect(screen.getAllByText("10")).toHaveLength(1); - }); - - resolve(); + render(); + void Promise.resolve().then(() => { + counterVar(1); + counterVar(2); + counterVar(3); + counterVar(4); + counterVar(5); + counterVar(6); + counterVar(7); + counterVar(8); + counterVar(9); + counterVar(10); + }); + + await waitFor(() => { + expect(screen.getAllByText("10")).toHaveLength(1); + }); + }); + + it("should survive many rerenderings despite racing asynchronous updates", (done) => { + const rv = makeVar(0); + + function App() { + const value = useReactiveVar(rv); + return ( +
+

{value}

+
+ ); } - ); - - itAsync( - "should survive many rerenderings despite racing asynchronous updates", - (resolve, reject) => { - const rv = makeVar(0); - - function App() { - const value = useReactiveVar(rv); - return ( -
-

{value}

-
- ); - } - const goalCount = 1000; - let updateCount = 0; - let stopped = false; - - function spam() { - if (stopped) return; - try { - if (++updateCount <= goalCount) { - act(() => { - rv(updateCount); - setTimeout(spam, Math.random() * 10); - }); - } else { - stopped = true; - expect(rv()).toBe(goalCount); - screen - .findByText(String(goalCount)) - .then((element) => { - expect(element.nodeName.toLowerCase()).toBe("h1"); - }) - .then(resolve, reject); - } - } catch (e) { + const goalCount = 1000; + let updateCount = 0; + let stopped = false; + + function spam() { + if (stopped) return; + try { + if (++updateCount <= goalCount) { + act(() => { + rv(updateCount); + setTimeout(spam, Math.random() * 10); + }); + } else { stopped = true; - reject(e); + expect(rv()).toBe(goalCount); + void screen + .findByText(String(goalCount)) + .then((element) => { + expect(element.nodeName.toLowerCase()).toBe("h1"); + }) + .then(done); } + } catch (e) { + stopped = true; + throw e; } - spam(); - spam(); - spam(); - spam(); - - render( - - - - ); } - ); + spam(); + spam(); + spam(); + spam(); + + render( + + + + ); + }); }); }); diff --git a/src/testing/internal/ObservableStream.ts b/src/testing/internal/ObservableStream.ts index 63f550827c..f6c53169b8 100644 --- a/src/testing/internal/ObservableStream.ts +++ b/src/testing/internal/ObservableStream.ts @@ -1,4 +1,7 @@ -import type { Observable } from "../../utilities/index.js"; +import type { + Observable, + ObservableSubscription, +} from "../../utilities/index.js"; import { ReadableStream } from "node:stream/web"; export interface TakeOptions { @@ -11,10 +14,12 @@ type ObservableEvent = export class ObservableStream { private reader: ReadableStreamDefaultReader>; + private subscription!: ObservableSubscription; + constructor(observable: Observable) { this.reader = new ReadableStream>({ - start(controller) { - observable.subscribe( + start: (controller) => { + this.subscription = observable.subscribe( (value) => controller.enqueue({ type: "next", value }), (error) => controller.enqueue({ type: "error", error }), () => controller.enqueue({ type: "complete" }) @@ -36,6 +41,10 @@ export class ObservableStream { ]); } + unsubscribe() { + this.subscription.unsubscribe(); + } + async takeNext(options?: TakeOptions): Promise { const event = await this.take(options); expect(event).toEqual({ type: "next", value: expect.anything() }); diff --git a/src/testing/matchers/toEmitError.ts b/src/testing/matchers/toEmitError.ts index 75e93aa56f..f488e6f0de 100644 --- a/src/testing/matchers/toEmitError.ts +++ b/src/testing/matchers/toEmitError.ts @@ -1,7 +1,15 @@ -import type { MatcherFunction } from "expect"; +import type { MatcherFunction, MatcherContext } from "expect"; import type { ObservableStream } from "../internal/index.js"; import type { TakeOptions } from "../internal/ObservableStream.js"; +function isErrorEqual(this: MatcherContext, expected: any, actual: any) { + if (typeof expected === "string" && actual instanceof Error) { + return actual.message === expected; + } + + return this.equals(expected, actual, this.customTesters); +} + export const toEmitError: MatcherFunction< [value?: any, options?: TakeOptions] > = async function (actual, expected, options) { @@ -15,9 +23,7 @@ export const toEmitError: MatcherFunction< try { const error = await stream.takeError(options); const pass = - expected === undefined ? true : ( - this.equals(expected, error, this.customTesters) - ); + expected === undefined ? true : isErrorEqual.call(this, expected, error); return { pass, @@ -37,7 +43,7 @@ export const toEmitError: MatcherFunction< "\n\n" + this.utils.printDiffOrStringify( expected, - error, + typeof expected === "string" ? error.message : error, "Expected", "Recieved", true diff --git a/src/utilities/observables/__tests__/asyncMap.ts b/src/utilities/observables/__tests__/asyncMap.ts index ce4227be45..8f9d53071c 100644 --- a/src/utilities/observables/__tests__/asyncMap.ts +++ b/src/utilities/observables/__tests__/asyncMap.ts @@ -1,6 +1,5 @@ import { Observable } from "../Observable"; import { asyncMap } from "../asyncMap"; -import { itAsync } from "../../../testing"; import { ObservableStream } from "../../../testing/internal"; const wait = (delayMs: number) => new Promise((resolve) => setTimeout(resolve, delayMs)); @@ -19,106 +18,66 @@ function make1234Observable() { }); } -function rejectExceptions( - reject: (reason: any) => any, - fn: (...args: Args) => Ret -) { - return function () { - try { - // @ts-expect-error - return fn.apply(this, arguments); - } catch (error) { - reject(error); - } - } as typeof fn; -} - describe("asyncMap", () => { - itAsync("keeps normal results in order", (resolve, reject) => { + it("keeps normal results in order", async () => { const values: number[] = []; - const mapped: number[] = []; - asyncMap(make1234Observable(), (value) => { + const observable = asyncMap(make1234Observable(), (value) => { values.push(value); // Make earlier results take longer than later results. const delay = 100 - value * 10; return wait(delay).then(() => value * 2); - }).subscribe({ - next(mappedValue) { - mapped.push(mappedValue); - }, - error: reject, - complete: rejectExceptions(reject, () => { - expect(values).toEqual([1, 2, 3, 4]); - expect(mapped).toEqual([2, 4, 6, 8]); - resolve(); - }), }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(2); + await expect(stream).toEmitValue(4); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitValue(8); + await expect(stream).toComplete(); + + expect(values).toEqual([1, 2, 3, 4]); }); - itAsync("handles exceptions from mapping functions", (resolve, reject) => { - const triples: number[] = []; - asyncMap(make1234Observable(), (num) => { + it("handles exceptions from mapping functions", async () => { + const observable = asyncMap(make1234Observable(), (num) => { if (num === 3) throw new Error("expected"); return num * 3; - }).subscribe({ - next: rejectExceptions(reject, (triple) => { - expect(triple).toBeLessThan(9); - triples.push(triple); - }), - error: rejectExceptions(reject, (error) => { - expect(error.message).toBe("expected"); - expect(triples).toEqual([3, 6]); - resolve(); - }), }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitError(new Error("expected")); + }); + + it("handles rejected promises from mapping functions", async () => { + const observable = asyncMap(make1234Observable(), (num) => { + if (num === 3) return Promise.reject(new Error("expected")); + return num * 3; + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitError(new Error("expected")); }); - itAsync( - "handles rejected promises from mapping functions", - (resolve, reject) => { - const triples: number[] = []; - asyncMap(make1234Observable(), (num) => { - if (num === 3) return Promise.reject(new Error("expected")); + it("handles async exceptions from mapping functions", async () => { + const observable = asyncMap(make1234Observable(), (num) => + wait(10).then(() => { + if (num === 3) throw new Error("expected"); return num * 3; - }).subscribe({ - next: rejectExceptions(reject, (triple) => { - expect(triple).toBeLessThan(9); - triples.push(triple); - }), - error: rejectExceptions(reject, (error) => { - expect(error.message).toBe("expected"); - expect(triples).toEqual([3, 6]); - resolve(); - }), - }); - } - ); + }) + ); + const stream = new ObservableStream(observable); - itAsync( - "handles async exceptions from mapping functions", - (resolve, reject) => { - const triples: number[] = []; - asyncMap(make1234Observable(), (num) => - wait(10).then(() => { - if (num === 3) throw new Error("expected"); - return num * 3; - }) - ).subscribe({ - next: rejectExceptions(reject, (triple) => { - expect(triple).toBeLessThan(9); - triples.push(triple); - }), - error: rejectExceptions(reject, (error) => { - expect(error.message).toBe("expected"); - expect(triples).toEqual([3, 6]); - resolve(); - }), - }); - } - ); + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitError(new Error("expected")); + }); - itAsync("handles exceptions from next functions", (resolve, reject) => { + it("handles exceptions from next functions", (done) => { const triples: number[] = []; asyncMap(make1234Observable(), (num) => { return num * 3; @@ -136,10 +95,10 @@ describe("asyncMap", () => { // expect(triples).toEqual([3, 6, 9]); // resolve(); // }), - complete: rejectExceptions(reject, () => { + complete: () => { expect(triples).toEqual([3, 6, 9, 12]); - resolve(); - }), + done(); + }, }); }); From 655d87d1f4130fea84bab3ed466ef327c526083c Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Thu, 12 Dec 2024 13:09:11 -0700 Subject: [PATCH 3/4] Move redirects --- docs/source/_redirects | 108 ----------------------------------------- 1 file changed, 108 deletions(-) delete mode 100644 docs/source/_redirects diff --git a/docs/source/_redirects b/docs/source/_redirects deleted file mode 100644 index 689c408a08..0000000000 --- a/docs/source/_redirects +++ /dev/null @@ -1,108 +0,0 @@ -# Redirect all 3.0 beta docs to root -/v3.0-beta/* /docs/react/:splat - -# Redirect 2.x docs to v2 -/v2.4/* /docs/react/v2/ -/v2.5/* /docs/react/v2/ -/v2.6/* /docs/react/v2/:splat - -# Split out pagination article -/data/pagination/ /docs/react/pagination/overview/ - -# Remove 'Recompose patterns' article -/development-testing/recompose/ /docs/react/ - -# Client 3.0 changes -/api/apollo-client/ /docs/react/api/core/ApolloClient/ -/api/react-hooks/ /docs/react/api/react/hooks/ -/api/react-testing/ /docs/react/api/react/testing/ -/api/react-components/ /docs/react/api/react/components/ -/api/react-hoc/ /docs/react/api/react/hoc/ -/api/react-ssr/ /docs/react/api/react/ssr/ -/api/react-common/ /docs/react/api/react/hooks/ - -# Apollo Client Information Architecture refresh -# https://github.com/apollographql/apollo-client/pull/5321 -/features/error-handling/ /docs/react/data/error-handling/ -/advanced/fragments/ /docs/react/data/fragments/ -/essentials/mutations/ /docs/react/data/mutations/ -/features/pagination/ /docs/react/data/pagination/ -/essentials/queries/ /docs/react/data/queries/ -/advanced/subscriptions/ /docs/react/data/subscriptions/ -/recipes/client-schema-mocking/ /docs/react/development-testing/client-schema-mocking/ -/features/developer-tooling/ /docs/react/development-testing/developer-tooling/ -/recipes/recompose/ /docs/react/ -/recipes/static-typing/ /docs/react/development-testing/static-typing/ -/recipes/testing/ /docs/react/development-testing/testing/ -/essentials/get-started/ /docs/react/get-started/ -/integrations/ /docs/react/integrations/integrations/ -/recipes/meteor/ /docs/react/integrations/meteor/ -/recipes/react-native/ /docs/react/integrations/react-native/ -/recipes/webpack/ /docs/react/integrations/webpack/ -/advanced/caching/ /docs/react/caching/cache-configuration/ -/essentials/local-state/ /docs/react/data/local-state/ -/advanced/boost-migration/ /docs/react/migrating/boost-migration/ -/hooks-migration/ /docs/react/migrating/hooks-migration/ -/recipes/authentication/ /docs/react/networking/authentication/ -/advanced/network-layer/ /docs/react/networking/network-layer/ -/recipes/babel/ /docs/react/performance/babel/ -/features/optimistic-ui/ /docs/react/performance/optimistic-ui/ -/recipes/performance/ /docs/react/performance/performance/ -/features/server-side-rendering/ /docs/react/performance/server-side-rendering/ - -# React Apollo 2.0 - Basics -# https://github.com/apollographql/apollo-client/pull/3097 -/basics/setup.html /docs/react/get-started/ -/essentials/get-started.html /docs/react/get-started/ -/basics/queries.html /docs/react/data/queries/ -/essentials/queries.html /docs/react/data/queries/ -/basics/mutations.html /docs/react/data/mutations/ -/essentials/mutations.html /docs/react/data/mutations/ -/basics/network-layer.html /docs/react/networking/network-layer/ -/advanced/network-layer.html /docs/react/networking/network-layer/ -/basics/caching.html /docs/react/caching/cache-configuration/ -/advanced/caching.html /docs/react/caching/cache-configuration/ - -# React Apollo 2.0 - Features -# https://github.com/apollographql/apollo-client/pull/3097 -/features/caching.html /docs/react/caching/cache-configuration/ -/advanced/caching.html /docs/react/caching/cache-configuration/ -/features/cache-updates.html /docs/react/caching/cache-configuration/ -/advanced/caching.html /docs/react/caching/cache-configuration/ -/features/fragments.html /docs/react/data/fragments/ -/advanced/fragments.html /docs/react/data/fragments/ -/features/subscriptions.html /docs/react/data/subscriptions/ -/advanced/subscriptions.html /docs/react/data/subscriptions/ -/features/react-native.html /docs/react/integrations/react-native/ -/recipes/react-native.html /docs/react/integrations/react-native/ -/features/static-typing.html /docs/react/development-testing/static-typing/ -/recipes/static-typing.html /docs/react/development-testing/static-typing/ -/features/error-handling.html /docs/react/data/error-handling/ - -# React Apollo 2.0 - Recipes -# https://github.com/apollographql/apollo-client/pull/3097 -/recipes/query-splitting.html /docs/react/performance/performance/ -/features/performance.html#query-splitting /docs/react/performance/performance/ -/recipes/pagination.html /docs/react/data/pagination/ -/features/pagination.html /docs/react/data/pagination/ -/recipes/prefetching.html /docs/react/performance/performance/ -/features/performance.html#prefetching /docs/react/performance/performance/ -/recipes/server-side-rendering.html /docs/react/performance/server-side-rendering/ -/features/server-side-rendering.html /docs/react/performance/server-side-rendering/ -/recipes/fragment-matching.html /docs/react/data/fragments/ -/advanced/fragments.html /docs/react/data/fragments/ - -# Ported from old _config.yml file -/essentials/get-started.html#api /docs/react/get-started/ -/api/react-apollo.html /docs/react/get-started/ -/essentials/queries.html#api /docs/react/data/queries/#options -docs/react/api/react-apollo.html#graphql-query-options /docs/react/data/queries/#options -/basics/mutations.html#api /docs/react/basics/mutations.html -/recipes/simple-example.html /docs/react/get-started/ -docs/react/essentials/get-started.html /docs/react/get-started/ -/api/apollo-client.html#FetchPolicy /docs/react/api/core/ApolloClient/#example-defaultoptions-object -docs/react/api/react-apollo.html#graphql-config-options-fetchPolicy /docs/react/api/core/ApolloClient/#example-defaultoptions-object -/api/apollo-client.html#ErrorPolicy /docs/react/api/core/ApolloClient/#example-defaultoptions-object -docs/react/api/react-apollo.html#graphql-config-options-errorPolicy /docs/react/api/core/ApolloClient/#example-defaultoptions-object -/features/performance.html /docs/react/performance/performance/ -/recipes/performance.html /docs/react/performance/performance/ From 07907ea2af9838637738ae621c219f6c8e9d877f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 12 Dec 2024 16:25:27 -0700 Subject: [PATCH 4/4] Fix incorrect config for client preset in docs (#12218) --- docs/source/data/fragments.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/data/fragments.mdx b/docs/source/data/fragments.mdx index 3a9627fe64..0faf4fcca7 100644 --- a/docs/source/data/fragments.mdx +++ b/docs/source/data/fragments.mdx @@ -1156,10 +1156,12 @@ const config: CodegenConfig = { // ... // disables the incompatible GraphQL Codegen fragment masking feature fragmentMasking: false, - inlineFragmentTypes: "mask", customDirectives: { apolloUnmask: true } + }, + config: { + inlineFragmentTypes: "mask", } } }