diff --git a/src/__tests__/__snapshots__/graphqlSubscriptions.ts.snap b/src/__tests__/__snapshots__/graphqlSubscriptions.ts.snap deleted file mode 100644 index 8497fae92f5..00000000000 --- a/src/__tests__/__snapshots__/graphqlSubscriptions.ts.snap +++ /dev/null @@ -1,63 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GraphQL Subscriptions should throw an error if the result has errors on it 1`] = ` -ApolloError { - "cause": Object { - "locations": Array [ - Object { - "column": 3, - "line": 2, - }, - ], - "message": "This is an error", - "path": Array [ - "result", - ], - }, - "clientErrors": Array [], - "extraInfo": undefined, - "graphQLErrors": Array [ - Object { - "locations": Array [ - Object { - "column": 3, - "line": 2, - }, - ], - "message": "This is an error", - "path": Array [ - "result", - ], - }, - ], - "message": "This is an error", - "name": "ApolloError", - "networkError": null, - "protocolErrors": Array [], -} -`; - -exports[`GraphQL Subscriptions should throw an error if the result has protocolErrors on it 1`] = ` -ApolloError { - "cause": Object { - "extensions": Object { - "code": "WEBSOCKET_MESSAGE_ERROR", - }, - "message": "cannot read message from websocket", - }, - "clientErrors": Array [], - "extraInfo": undefined, - "graphQLErrors": Array [], - "message": "cannot read message from websocket", - "name": "ApolloError", - "networkError": null, - "protocolErrors": Array [ - Object { - "extensions": Object { - "code": "WEBSOCKET_MESSAGE_ERROR", - }, - "message": "cannot read message from websocket", - }, - ], -} -`; diff --git a/src/__tests__/graphqlSubscriptions.ts b/src/__tests__/graphqlSubscriptions.ts index d553f3b802d..4c28cc649be 100644 --- a/src/__tests__/graphqlSubscriptions.ts +++ b/src/__tests__/graphqlSubscriptions.ts @@ -1,13 +1,11 @@ import gql from "graphql-tag"; -import { ApolloClient, FetchResult } from "../core"; +import { ApolloClient } from "../core"; import { InMemoryCache } from "../cache"; import { ApolloError, PROTOCOL_ERRORS_SYMBOL } from "../errors"; -import { QueryManager } from "../core/QueryManager"; import { mockObservableLink } from "../testing"; import { GraphQLError } from "graphql"; import { ObservableStream, spyOnConsole } from "../testing/internal"; -import { getDefaultOptionsForQueryManagerTests } from "../testing/core/mocking/mockQueryManager"; describe("GraphQL Subscriptions", () => { const results = [ @@ -58,7 +56,7 @@ describe("GraphQL Subscriptions", () => { const stream = new ObservableStream(client.subscribe(defaultOptions)); link.simulateResult(results[0]); - await expect(stream).toEmitValue(results[0].result); + await expect(stream).toEmitFetchResult(results[0].result); stream.unsubscribe(); }); @@ -75,51 +73,45 @@ describe("GraphQL Subscriptions", () => { link.simulateResult(results[0]); - await expect(stream).toEmitValue(results[0].result); + await expect(stream).toEmitFetchResult(results[0].result); stream.unsubscribe(); }); it("should multiplex subscriptions", async () => { const link = mockObservableLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache: new InMemoryCache({ addTypename: false }), - }) - ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const obs = queryManager.startGraphQLSubscription(options); + const obs = client.subscribe(options); const stream1 = new ObservableStream(obs); const stream2 = new ObservableStream(obs); link.simulateResult(results[0]); - await expect(stream1).toEmitValue(results[0].result); - await expect(stream2).toEmitValue(results[0].result); + await expect(stream1).toEmitFetchResult(results[0].result); + await expect(stream2).toEmitFetchResult(results[0].result); }); it("should receive multiple results for a subscription", async () => { const link = mockObservableLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache: new InMemoryCache({ addTypename: false }), - }) - ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const stream = new ObservableStream( - queryManager.startGraphQLSubscription(options) - ); + const stream = new ObservableStream(client.subscribe(options)); for (let i = 0; i < 4; i++) { link.simulateResult(results[i]); } - await expect(stream).toEmitValue(results[0].result); - await expect(stream).toEmitValue(results[1].result); - await expect(stream).toEmitValue(results[2].result); - await expect(stream).toEmitValue(results[3].result); + await expect(stream).toEmitFetchResult(results[0].result); + await expect(stream).toEmitFetchResult(results[1].result); + await expect(stream).toEmitFetchResult(results[2].result); + await expect(stream).toEmitFetchResult(results[3].result); await expect(stream).not.toEmitAnything(); }); @@ -142,30 +134,17 @@ describe("GraphQL Subscriptions", () => { expect(cache.extract()).toEqual({}); }); - it("should throw an error if the result has errors on it", () => { + it("should throw an error if the result has errors on it", async () => { const link = mockObservableLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache: new InMemoryCache({ addTypename: false }), - }) - ); - - const obs = queryManager.startGraphQLSubscription(options); - - const promise = new Promise((resolve, reject) => { - obs.subscribe({ - next(result) { - reject("Should have hit the error block"); - }, - error(error) { - expect(error).toMatchSnapshot(); - resolve(); - }, - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), }); - const errorResult = { + const obs = client.subscribe(options); + const stream = new ObservableStream(obs); + + link.simulateResult({ result: { data: null, errors: [ @@ -181,10 +160,24 @@ describe("GraphQL Subscriptions", () => { } as any, ], }, - }; + }); - link.simulateResult(errorResult); - return Promise.resolve(promise); + await expect(stream).toEmitError( + new ApolloError({ + graphQLErrors: [ + { + message: "This is an error", + locations: [ + { + column: 3, + line: 2, + }, + ], + path: ["result"], + }, + ], + }) + ); }); it('returns errors in next result when `errorPolicy` is "all"', async () => { @@ -196,44 +189,34 @@ describe("GraphQL Subscriptions", () => { } `; const link = mockObservableLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache: new InMemoryCache(), - }) - ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - const obs = queryManager.startGraphQLSubscription({ + const obs = client.subscribe({ query, variables: { name: "Iron Man" }, errorPolicy: "all", }); + const stream = new ObservableStream(obs); - const promise = new Promise((resolve, reject) => { - const results: FetchResult[] = []; - - obs.subscribe({ - next: (result) => results.push(result), - complete: () => resolve(results), - error: reject, - }); - }); - - const errorResult = { - result: { - data: null, - errors: [new GraphQLError("This is an error")], + link.simulateResult( + { + result: { + data: null, + errors: [new GraphQLError("This is an error")], + }, }, - }; + true + ); - link.simulateResult(errorResult, true); + await expect(stream).toEmitFetchResult({ + data: null, + errors: [new GraphQLError("This is an error")], + }); - await expect(promise).resolves.toEqual([ - { - data: null, - errors: [new GraphQLError("This is an error")], - }, - ]); + await expect(stream).toComplete(); }); it('throws protocol errors when `errorPolicy` is "all"', async () => { @@ -245,51 +228,41 @@ describe("GraphQL Subscriptions", () => { } `; const link = mockObservableLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache: new InMemoryCache(), - }) - ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - const obs = queryManager.startGraphQLSubscription({ + const obs = client.subscribe({ query, variables: { name: "Iron Man" }, errorPolicy: "all", }); + const stream = new ObservableStream(obs); - const promise = new Promise((resolve, reject) => { - const results: FetchResult[] = []; - - obs.subscribe({ - next: (result) => results.push(result), - complete: () => resolve(results), - error: reject, - }); - }); + // Silence expected warning about missing field for cache write + using _consoleSpy = spyOnConsole("warn"); - const errorResult = { - result: { - data: null, - extensions: { - [PROTOCOL_ERRORS_SYMBOL]: [ - { - message: "cannot read message from websocket", - extensions: { - code: "WEBSOCKET_MESSAGE_ERROR", + link.simulateResult( + { + result: { + data: null, + extensions: { + [PROTOCOL_ERRORS_SYMBOL]: [ + { + message: "cannot read message from websocket", + extensions: { + code: "WEBSOCKET_MESSAGE_ERROR", + }, }, - } as any, - ], + ], + }, }, }, - }; - - // Silence expected warning about missing field for cache write - using _consoleSpy = spyOnConsole("warn"); - - link.simulateResult(errorResult, true); + true + ); - await expect(promise).rejects.toEqual( + await expect(stream).toEmitError( new ApolloError({ protocolErrors: [ { @@ -312,39 +285,30 @@ describe("GraphQL Subscriptions", () => { } `; const link = mockObservableLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache: new InMemoryCache(), - }) - ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - const obs = queryManager.startGraphQLSubscription({ + const obs = client.subscribe({ query, variables: { name: "Iron Man" }, errorPolicy: "ignore", }); + const stream = new ObservableStream(obs); - const promise = new Promise((resolve, reject) => { - const results: FetchResult[] = []; - - obs.subscribe({ - next: (result) => results.push(result), - complete: () => resolve(results), - error: reject, - }); - }); - - const errorResult = { - result: { - data: null, - errors: [new GraphQLError("This is an error")], + link.simulateResult( + { + result: { + data: null, + errors: [new GraphQLError("This is an error")], + }, }, - }; - - link.simulateResult(errorResult, true); + true + ); - await expect(promise).resolves.toEqual([{ data: null }]); + await expect(stream).toEmitFetchResult({ data: null }); + await expect(stream).toComplete(); }); it('throws protocol errors when `errorPolicy` is "ignore"', async () => { @@ -356,51 +320,41 @@ describe("GraphQL Subscriptions", () => { } `; const link = mockObservableLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache: new InMemoryCache(), - }) - ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - const obs = queryManager.startGraphQLSubscription({ + const obs = client.subscribe({ query, variables: { name: "Iron Man" }, errorPolicy: "ignore", }); + const stream = new ObservableStream(obs); - const promise = new Promise((resolve, reject) => { - const results: FetchResult[] = []; - - obs.subscribe({ - next: (result) => results.push(result), - complete: () => resolve(results), - error: reject, - }); - }); + // Silence expected warning about missing field for cache write + using _consoleSpy = spyOnConsole("warn"); - const errorResult = { - result: { - data: null, - extensions: { - [PROTOCOL_ERRORS_SYMBOL]: [ - { - message: "cannot read message from websocket", - extensions: { - code: "WEBSOCKET_MESSAGE_ERROR", + link.simulateResult( + { + result: { + data: null, + extensions: { + [PROTOCOL_ERRORS_SYMBOL]: [ + { + message: "cannot read message from websocket", + extensions: { + code: "WEBSOCKET_MESSAGE_ERROR", + }, }, - }, - ], + ], + }, }, }, - }; - - // Silence expected warning about missing field for cache write - using _consoleSpy = spyOnConsole("warn"); - - link.simulateResult(errorResult, true); + true + ); - await expect(promise).rejects.toEqual( + await expect(stream).toEmitError( new ApolloError({ protocolErrors: [ { @@ -414,21 +368,18 @@ describe("GraphQL Subscriptions", () => { ); }); - it("should call complete handler when the subscription completes", () => { + it("should call complete handler when the subscription completes", async () => { const link = mockObservableLink(); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), }); - return new Promise((resolve) => { - client.subscribe(defaultOptions).subscribe({ - complete() { - resolve(); - }, - }); - setTimeout(() => link.simulateComplete(), 100); - }); + const stream = new ObservableStream(client.subscribe(defaultOptions)); + + setTimeout(() => link.simulateComplete(), 50); + + await expect(stream).toComplete(); }); it("should pass a context object through the link execution chain", async () => { @@ -442,7 +393,8 @@ describe("GraphQL Subscriptions", () => { link.simulateResult(results[0]); - await expect(stream).toEmitNext(); + await expect(stream).toEmitFetchResult(results[0].result); + expect(link.operation?.getContext().someVar).toEqual( options.context.someVar ); @@ -450,28 +402,18 @@ describe("GraphQL Subscriptions", () => { it("should throw an error if the result has protocolErrors on it", async () => { const link = mockObservableLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache: new InMemoryCache({ addTypename: false }), - }) - ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const obs = queryManager.startGraphQLSubscription(options); + const obs = client.subscribe(options); + const stream = new ObservableStream(obs); - const promise = new Promise((resolve, reject) => { - obs.subscribe({ - next(result) { - reject("Should have hit the error block"); - }, - error(error) { - expect(error).toMatchSnapshot(); - resolve(); - }, - }); - }); + // Silence expected warning about missing field for cache write + using _consoleSpy = spyOnConsole("warn"); - const errorResult = { + link.simulateResult({ result: { data: null, extensions: { @@ -485,13 +427,19 @@ describe("GraphQL Subscriptions", () => { ], }, }, - }; - - // Silence expected warning about missing field for cache write - using _consoleSpy = spyOnConsole("warn"); - - link.simulateResult(errorResult); + }); - await promise; + await expect(stream).toEmitError( + new ApolloError({ + protocolErrors: [ + { + message: "cannot read message from websocket", + extensions: { + code: "WEBSOCKET_MESSAGE_ERROR", + }, + }, + ], + }) + ); }); }); diff --git a/src/__tests__/local-state/resolvers.ts b/src/__tests__/local-state/resolvers.ts index b1941cd80c7..401f24b60b7 100644 --- a/src/__tests__/local-state/resolvers.ts +++ b/src/__tests__/local-state/resolvers.ts @@ -3,16 +3,19 @@ import { DocumentNode, ExecutionResult } from "graphql"; import { LocalState } from "../../core/LocalState"; -import { ApolloClient, ApolloQueryResult, Resolvers } from "../../core"; +import { + ApolloClient, + ApolloQueryResult, + NetworkStatus, + Resolvers, +} from "../../core"; import { InMemoryCache, isReference } from "../../cache"; import { Observable } from "../../utilities"; import { ApolloLink } from "../../link/core"; -import mockQueryManager from "../../testing/core/mocking/mockQueryManager"; import { ObservableStream } from "../../testing/internal"; +import { MockLink } from "../../testing"; -// Helper method that sets up a mockQueryManager and then passes on the -// results to an observer. const setupTestWithResolvers = ({ resolvers, query, @@ -32,17 +35,22 @@ const setupTestWithResolvers = ({ serverResult?: ExecutionResult; delay?: number; }) => { - const queryManager = mockQueryManager({ - request: { query: serverQuery || query, variables }, - result: serverResult, - error, - delay, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query: serverQuery || query, variables }, + result: serverResult, + error, + delay, + }, + ]), }); - queryManager.getLocalState().addResolvers(resolvers); + client.addResolvers(resolvers); return new ObservableStream( - queryManager.watchQuery({ query, variables, ...queryOptions }) + client.watchQuery({ query, variables, ...queryOptions }) ); }; @@ -64,7 +72,13 @@ describe("Basic resolver capabilities", () => { const stream = setupTestWithResolvers({ resolvers, query }); - await expect(stream).toEmitMatchedValue({ data: { foo: { bar: true } } }); + await expect(stream).toEmitApolloQueryResult({ + data: { foo: { bar: true } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); }); it("should handle queries with a mix of @client and server fields", async () => { @@ -100,12 +114,16 @@ describe("Basic resolver capabilities", () => { serverResult: { data: { bar: { baz: true } } }, }); - await expect(stream).toEmitMatchedValue({ + await expect(stream).toEmitApolloQueryResult({ data: { foo: { bar: true }, bar: { baz: true }, }, + loading: false, + networkStatus: NetworkStatus.ready, }); + + await expect(stream).not.toEmitAnything(); }); it("should handle a mix of @client fields with fragments and server fields", async () => { @@ -146,12 +164,16 @@ describe("Basic resolver capabilities", () => { serverResult: { data: { bar: { baz: true, __typename: "Bar" } } }, }); - await expect(stream).toEmitMatchedValue({ + await expect(stream).toEmitApolloQueryResult({ data: { foo: { bar: true, __typename: "ClientData" }, bar: { baz: true }, }, + loading: false, + networkStatus: NetworkStatus.ready, }); + + await expect(stream).not.toEmitAnything(); }); it("should handle @client fields inside fragments", async () => { @@ -203,12 +225,16 @@ describe("Basic resolver capabilities", () => { }, }); - await expect(stream).toEmitMatchedValue({ + await expect(stream).toEmitApolloQueryResult({ data: { foo: { bar: true, baz: false, __typename: "Foo" }, bar: { baz: true }, }, + loading: false, + networkStatus: NetworkStatus.ready, }); + + await expect(stream).not.toEmitAnything(); }); it("should have access to query variables when running @client resolvers", async () => { @@ -235,7 +261,13 @@ describe("Basic resolver capabilities", () => { variables: { id: 1 }, }); - await expect(stream).toEmitMatchedValue({ data: { foo: { bar: 1 } } }); + await expect(stream).toEmitApolloQueryResult({ + data: { foo: { bar: 1 } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); }); it("should pass context to @client resolvers", async () => { @@ -262,7 +294,13 @@ describe("Basic resolver capabilities", () => { queryOptions: { context: { id: 1 } }, }); - await expect(stream).toEmitMatchedValue({ data: { foo: { bar: 1 } } }); + await expect(stream).toEmitApolloQueryResult({ + data: { foo: { bar: 1 } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); }); it("should combine local @client resolver results with server results, for the same field", async () => { @@ -313,7 +351,7 @@ describe("Basic resolver capabilities", () => { }, }); - await expect(stream).toEmitMatchedValue({ + await expect(stream).toEmitApolloQueryResult({ data: { author: { name: "John Smith", @@ -323,7 +361,11 @@ describe("Basic resolver capabilities", () => { }, }, }, + loading: false, + networkStatus: NetworkStatus.ready, }); + + await expect(stream).not.toEmitAnything(); }); it("should handle resolvers that work with booleans properly", async () => { @@ -345,9 +387,13 @@ describe("Basic resolver capabilities", () => { }, }); - const { data } = await client.query({ query, fetchPolicy: "network-only" }); + const result = await client.query({ query, fetchPolicy: "network-only" }); - expect(data).toMatchObject({ isInCart: false }); + expect(result).toEqualApolloQueryResult({ + data: { isInCart: false }, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); it("should handle nested asynchronous @client resolvers (issue #4841)", () => { @@ -508,15 +554,17 @@ describe("Basic resolver capabilities", () => { serverResult: { data: { bar: { baz: true } } }, }); - await expect(stream).toEmitMatchedValue({ + await expect(stream).toEmitApolloQueryResult({ data: { foo: { bar: true }, bar: { baz: true } }, + loading: false, + networkStatus: NetworkStatus.ready, }); expect(barResolver).not.toHaveBeenCalled(); }); }); describe("Writing cache data from resolvers", () => { - it("should let you write to the cache with a mutation", () => { + it("should let you write to the cache with a mutation", async () => { const query = gql` { field @client @@ -542,15 +590,17 @@ describe("Writing cache data from resolvers", () => { }, }); - return client - .mutate({ mutation }) - .then(() => client.query({ query })) - .then(({ data }) => { - expect({ ...data }).toMatchObject({ field: 1 }); - }); + await client.mutate({ mutation }); + const result = await client.query({ query }); + + expect(result).toEqualApolloQueryResult({ + data: { field: 1 }, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); - it("should let you write to the cache with a mutation using an ID", () => { + it("should let you write to the cache with a mutation using an ID", async () => { const query = gql` { obj @client { @@ -596,15 +646,18 @@ describe("Writing cache data from resolvers", () => { }, }); - return client - .mutate({ mutation }) - .then(() => client.query({ query })) - .then(({ data }: any) => { - expect(data.obj.field).toEqual(2); - }); + await client.mutate({ mutation }); + + const result = await client.query({ query }); + + expect(result).toEqualApolloQueryResult({ + data: { obj: { __typename: "Object", field: 2 } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); - it("should not overwrite __typename when writing to the cache with an id", () => { + it("should not overwrite __typename when writing to the cache with an id", async () => { const query = gql` { obj @client { @@ -658,14 +711,20 @@ describe("Writing cache data from resolvers", () => { }, }); - return client - .mutate({ mutation }) - .then(() => client.query({ query })) - .then(({ data }: any) => { - expect(data.obj.__typename).toEqual("Object"); - expect(data.obj.field.__typename).toEqual("Field"); - }) - .catch((e) => console.log(e)); + await client.mutate({ mutation }); + const result = await client.query({ query }); + + expect(result).toEqualApolloQueryResult({ + data: { + obj: { + __typename: "Object", + field: { __typename: "Field", field2: 2 }, + id: "uniqueId", + }, + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); }); @@ -698,11 +757,15 @@ describe("Resolving field aliases", () => { }, }); - const { data } = await client.query({ query }); + const result = await client.query({ query }); - expect(data).toEqual({ - foo: { bar: true, __typename: "Foo" }, - baz: { foo: true, __typename: "Baz" }, + expect(result).toEqualApolloQueryResult({ + data: { + foo: { bar: true, __typename: "Foo" }, + baz: { foo: true, __typename: "Baz" }, + }, + loading: false, + networkStatus: NetworkStatus.ready, }); }); @@ -727,9 +790,13 @@ describe("Resolving field aliases", () => { }, }); - const { data } = await client.query({ query: aliasedQuery }); + const result = await client.query({ query: aliasedQuery }); - expect(data).toEqual({ fie: { bar: true, __typename: "Foo" } }); + expect(result).toEqualApolloQueryResult({ + data: { fie: { bar: true, __typename: "Foo" } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); expect(fie).not.toHaveBeenCalled(); }); @@ -761,11 +828,15 @@ describe("Resolving field aliases", () => { }, }); - const { data } = await client.query({ query: aliasedQuery }); + const result = await client.query({ query: aliasedQuery }); - expect(data).toEqual({ - fie: { fum: true, __typename: "Foo" }, - baz: { foo: true, __typename: "Baz" }, + expect(result).toEqualApolloQueryResult({ + data: { + fie: { fum: true, __typename: "Foo" }, + baz: { foo: true, __typename: "Baz" }, + }, + loading: false, + networkStatus: NetworkStatus.ready, }); expect(fie).not.toHaveBeenCalled(); }); @@ -802,10 +873,12 @@ describe("Resolving field aliases", () => { }, }); - const { data } = await client.query({ query }); + const result = await client.query({ query }); - expect({ ...data }).toMatchObject({ - fie: { bar: "yo", __typename: "Foo" }, + expect(result).toEqualApolloQueryResult({ + data: { fie: { bar: "yo", __typename: "Foo" } }, + loading: false, + networkStatus: NetworkStatus.ready, }); }); @@ -862,19 +935,21 @@ describe("Resolving field aliases", () => { }, }); - { - 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); - } + // `isInCart` resolver is fired, returning `true` (which is then + // stored in the cache). + await expect(client.query({ query })).resolves.toEqualApolloQueryResult({ + data: { launch: { __typename: "Launch", id: 1, isInCart: true } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); - { - 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); - } + // When the same query fires again, `isInCart` should be pulled from + // the cache and have a value of `true`. + await expect(client.query({ query })).resolves.toEqualApolloQueryResult({ + data: { launch: { __typename: "Launch", id: 1, isInCart: true } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); }); @@ -909,8 +984,13 @@ describe("Force local resolvers", () => { // 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); + await expect(client.query({ query })).resolves.toEqualApolloQueryResult({ + data: { + author: { __typename: "Author", isLoggedIn: false, name: "John Smith" }, + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); client.addResolvers({ Author: { @@ -923,8 +1003,13 @@ describe("Force local resolvers", () => { // 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); + await expect(client.query({ query })).resolves.toEqualApolloQueryResult({ + data: { + author: { __typename: "Author", isLoggedIn: true, name: "John Smith" }, + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); it("should avoid running forced resolvers a second time when loading results over the network (so not from the cache)", async () => { @@ -962,8 +1047,17 @@ describe("Force local resolvers", () => { }, }); - const { data } = await client.query({ query }); - expect(data.author.isLoggedIn).toEqual(true); + await expect(client.query({ query })).resolves.toEqualApolloQueryResult({ + data: { + author: { + __typename: "Author", + isLoggedIn: true, + name: "John Smith", + }, + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); expect(count).toEqual(1); }); @@ -1090,13 +1184,17 @@ describe("Force local resolvers", () => { const result = await client.query({ query }); - expect(result.data).toEqual({ - userData: { - __typename: "User", - firstName: "Ben", - lastName: "Newman", - fullName: "Ben Newman", + expect(result).toEqualApolloQueryResult({ + data: { + userData: { + __typename: "User", + firstName: "Ben", + lastName: "Newman", + fullName: "Ben Newman", + }, }, + loading: false, + networkStatus: NetworkStatus.ready, }); }); }); @@ -1120,11 +1218,13 @@ describe("Async resolvers", () => { }, }); - const { - data: { isLoggedIn }, - } = await client.query({ query })!; + const result = await client.query({ query })!; - expect(isLoggedIn).toBe(true); + expect(result).toEqualApolloQueryResult({ + data: { isLoggedIn: true }, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); it("should support async @client resolvers mixed with remotely resolved data", async () => { @@ -1170,13 +1270,20 @@ describe("Async resolvers", () => { }, }); - const { - data: { member }, - } = await client.query({ query })!; + const result = await client.query({ query })!; - expect(member.name).toBe(testMember.name); - expect(member.isLoggedIn).toBe(testMember.isLoggedIn); - expect(member.sessionCount).toBe(testMember.sessionCount); + expect(result).toEqualApolloQueryResult({ + data: { + member: { + name: testMember.name, + isLoggedIn: testMember.isLoggedIn, + sessionCount: testMember.sessionCount, + __typename: "Member", + }, + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); }); diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index 04e4f912383..133165db818 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -17,33 +17,15 @@ import { removeDirectivesFromDocument, } from "../../utilities"; import { ApolloLink, FetchResult } from "../../link/core"; -import { InMemoryCache, NormalizedCacheObject } from "../../cache"; +import { InMemoryCache } from "../../cache"; import { ApolloError } from "../../errors"; -import { - MockLink, - mockSingleLink, - MockSubscriptionLink, - tick, - wait, -} from "../../testing"; -import mockQueryManager, { - getDefaultOptionsForQueryManagerTests, -} from "../../testing/core/mocking/mockQueryManager"; -import mockWatchQuery from "../../testing/core/mocking/mockWatchQuery"; +import { MockLink, MockSubscriptionLink, tick, wait } from "../../testing"; import { SubscriptionObserver } from "zen-observable-ts"; import { waitFor } from "@testing-library/react"; import { ObservableStream, spyOnConsole } from "../../testing/internal"; -function resetStore(qm: QueryManager) { - return qm - .clearStore({ - discardWatches: false, - }) - .then(() => qm.reFetchObservableQueries()); -} - export const mockFetchQuery = (queryManager: QueryManager) => { const fetchConcastWithInfo = queryManager["fetchConcastWithInfo"]; const fetchQueryByPolicy: QueryManager["fetchQueryByPolicy"] = ( @@ -103,37 +85,28 @@ describe("ObservableQuery", () => { graphQLErrors: [error], }); - const createQueryManager = ({ link }: { link: ApolloLink }) => { - return new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - assumeImmutableResults: true, - cache: new InMemoryCache({ - addTypename: false, - }), - }) - ); - }; - describe("setOptions", () => { describe("to change pollInterval", () => { it("starts polling if goes from 0 -> something", async () => { - const manager = mockQueryManager( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables }, - result: { data: dataTwo }, - }, - { - request: { query, variables }, - result: { data: dataTwo }, - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables }, + result: { data: dataTwo }, + }, + { + request: { query, variables }, + result: { data: dataTwo }, + }, + ]), + }); - const observable = manager.watchQuery({ + const observable = client.watchQuery({ query, variables, notifyOnNetworkStatusChange: false, @@ -141,19 +114,19 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const { data } = await stream.takeNext(); - - expect(data).toEqual(dataOne); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.setOptions({ query, pollInterval: 10 }); - { - const { data } = await stream.takeNext(); - - expect(data).toEqual(dataTwo); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); observable.stopPolling(); @@ -161,22 +134,25 @@ describe("ObservableQuery", () => { }); it("stops polling if goes from something -> 0", async () => { - const manager = mockQueryManager( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables }, - result: { data: dataTwo }, - }, - { - request: { query, variables }, - result: { data: dataTwo }, - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables }, + result: { data: dataTwo }, + }, + { + request: { query, variables }, + result: { data: dataTwo }, + }, + ]), + }); - const observable = manager.watchQuery({ + const observable = client.watchQuery({ query, variables, pollInterval: 10, @@ -184,11 +160,11 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const { data } = await stream.takeNext(); - - expect(data).toEqual(dataOne); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.setOptions({ query, pollInterval: 0 }); @@ -196,22 +172,25 @@ describe("ObservableQuery", () => { }); it("can change from x>0 to y>0", async () => { - const manager = mockQueryManager( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables }, - result: { data: dataTwo }, - }, - { - request: { query, variables }, - result: { data: dataTwo }, - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables }, + result: { data: dataTwo }, + }, + { + request: { query, variables }, + result: { data: dataTwo }, + }, + ]), + }); - const observable = manager.watchQuery({ + const observable = client.watchQuery({ query, variables, pollInterval: 100, @@ -220,19 +199,19 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const { data } = await stream.takeNext(); - - expect(data).toEqual(dataOne); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.setOptions({ query, pollInterval: 10 }); - { - const { data } = await stream.takeNext(); - - expect(data).toEqual(dataTwo); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); observable.stopPolling(); @@ -258,24 +237,27 @@ describe("ObservableQuery", () => { const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } }; const variables2 = { first: 1 }; - const queryManager = mockQueryManager( - { - request: { - query: queryWithVars, - variables: variables1, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { + query: queryWithVars, + variables: variables1, + }, + result: { data }, }, - result: { data }, - }, - { - request: { - query: queryWithVars, - variables: variables2, + { + request: { + query: queryWithVars, + variables: variables2, + }, + result: { data: data2 }, }, - result: { data: data2 }, - } - ); + ]), + }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query: queryWithVars, variables: variables1, notifyOnNetworkStatusChange: true, @@ -283,28 +265,27 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const { data, loading } = await stream.takeNext(); - - expect(data).toEqual(data); - expect(loading).toBe(false); - } + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.refetch(variables2); - { - const { loading, networkStatus } = await stream.takeNext(); - - expect(loading).toBe(true); - expect(networkStatus).toBe(NetworkStatus.setVariables); - } - - { - const { data, loading } = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + // TODO: Ensure this value is undefined instead of an empty object + data: {}, + loading: true, + networkStatus: NetworkStatus.setVariables, + partial: true, + }); - expect(loading).toBe(false); - expect(data).toEqual(data2); - } + await expect(stream).toEmitApolloQueryResult({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); @@ -326,24 +307,27 @@ describe("ObservableQuery", () => { const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } }; - const queryManager = mockQueryManager( - { - request: { - query, - variables, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { + query, + variables, + }, + result: { data }, }, - result: { data }, - }, - { - request: { - query, - variables, + { + request: { + query, + variables, + }, + result: { data: data2 }, }, - result: { data: data2 }, - } - ); + ]), + }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query, variables, notifyOnNetworkStatusChange: true, @@ -351,28 +335,25 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.loading).toEqual(false); - expect(result.data).toEqual(data); - } + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.refetch(); - { - const { loading, networkStatus } = await stream.takeNext(); - - expect(loading).toEqual(true); - expect(networkStatus).toEqual(NetworkStatus.refetch); - } - - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data, + loading: true, + networkStatus: NetworkStatus.refetch, + }); - expect(result.loading).toEqual(false); - expect(result.data).toEqual(data2); - } + await expect(stream).toEmitApolloQueryResult({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); @@ -395,118 +376,138 @@ describe("ObservableQuery", () => { const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } }; const variables2 = { first: 1 }; - const observable: ObservableQuery = mockWatchQuery( - { - request: { - query, - variables, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data }, }, - result: { data }, - }, - { - request: { - query, - variables: variables2, + { + request: { query, variables: variables2 }, + result: { data: data2 }, }, - result: { data: data2 }, - } - ); - + ]), + }); + const observable = client.watchQuery({ query, variables }); const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.data).toEqual(data); - expect(result.loading).toBe(false); - } + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.setOptions({ variables: variables2, notifyOnNetworkStatusChange: true, }); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.setVariables); - } - - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data: {}, + loading: true, + networkStatus: NetworkStatus.setVariables, + partial: true, + }); - expect(result.loading).toBe(false); - expect(result.data).toEqual(data2); - } + await expect(stream).toEmitApolloQueryResult({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); // go back to first set of variables const current = await observable.reobserve({ variables }); - expect(current.data).toEqual(data); + expect(current).toEqualApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); }); it("if query is refetched, and an error is returned, no other observer callbacks will be called", async () => { - const observable = mockWatchQuery( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables }, - result: { errors: [error] }, - }, - { - request: { query, variables }, - result: { data: dataOne }, - } - ); - + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables }, + result: { errors: [error] }, + }, + { + request: { query, variables }, + result: { data: dataOne }, + }, + ]), + }); + const observable = client.watchQuery({ query, variables }); const stream = new ObservableStream(observable); - { - const { data } = await stream.takeNext(); - - expect(data).toEqual(dataOne); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); - await observable.refetch().catch(() => {}); + await expect(observable.refetch()).rejects.toThrow( + new ApolloError({ graphQLErrors: [error] }) + ); - await stream.takeError(); + await expect(stream).toEmitError( + new ApolloError({ graphQLErrors: [error] }) + ); - await observable.refetch(); + await expect(observable.refetch()).resolves.toEqualApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); it("does a network request if fetchPolicy becomes networkOnly", async () => { - const observable = mockWatchQuery( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables }, - result: { data: dataTwo }, - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables }, + result: { data: dataTwo }, + }, + ]), + }); + const observable = client.watchQuery({ query, variables }); const stream = new ObservableStream(observable); - { - const { data, loading } = await stream.takeNext(); - - expect(loading).toEqual(false); - expect(data).toEqual(dataOne); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.setOptions({ fetchPolicy: "network-only" }); - { - const { data, loading } = await stream.takeNext(); - - expect(loading).toEqual(false); - expect(data).toEqual(dataTwo); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); @@ -537,32 +538,36 @@ describe("ObservableQuery", () => { }), ]); - const queryManager = createQueryManager({ link }); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link, + }); // fetch first data from server - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query: testQuery, }); const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - } + expect(timesFired).toBe(1); await observable.setOptions({ fetchPolicy: "cache-only" }); - await resetStore(queryManager); + await client.resetStore(); - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data: {}, + loading: false, + networkStatus: NetworkStatus.ready, + partial: true, + }); - expect(result.data).toEqual({}); - expect(result.loading).toBe(false); - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(timesFired).toBe(1); - } + expect(timesFired).toBe(1); await expect(stream).not.toEmitAnything(); }); @@ -594,9 +599,12 @@ describe("ObservableQuery", () => { }, ]); - const queryManager = createQueryManager({ link }); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link, + }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query: testQuery, fetchPolicy: "cache-only", notifyOnNetworkStatusChange: false, @@ -604,30 +612,26 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(false); - expect(result.data).toEqual({}); - expect(timesFired).toBe(0); - } + await expect(stream).toEmitApolloQueryResult({ + data: {}, + loading: false, + networkStatus: NetworkStatus.ready, + partial: true, + }); + expect(timesFired).toBe(0); await observable.setOptions({ fetchPolicy: "cache-first" }); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(false); - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - } - + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(timesFired).toBe(1); await expect(stream).not.toEmitAnything(); }); it("can set queries to standby and will not fetch when doing so", async () => { - let queryManager: QueryManager; - let observable: ObservableQuery; const testQuery = gql` query { author { @@ -654,8 +658,11 @@ describe("ObservableQuery", () => { }); }, ]); - queryManager = createQueryManager({ link }); - observable = queryManager.watchQuery({ + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link, + }); + const observable = client.watchQuery({ query: testQuery, fetchPolicy: "cache-first", notifyOnNetworkStatusChange: false, @@ -663,23 +670,21 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - } + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(timesFired).toBe(1); await observable.setOptions({ query, fetchPolicy: "standby" }); - // make sure the query didn't get fired again. - expect(timesFired).toBe(1); + // make sure the query didn't get fired again. await expect(stream).not.toEmitAnything(); + expect(timesFired).toBe(1); }); it("will not fetch when setting a cache-only query to standby", async () => { - let queryManager: QueryManager; - let observable: ObservableQuery; const testQuery = gql` query { author { @@ -706,72 +711,102 @@ describe("ObservableQuery", () => { }); }, ]); - queryManager = createQueryManager({ link }); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link, + }); - await queryManager.query({ query: testQuery }); + await client.query({ query: testQuery }); - observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query: testQuery, - fetchPolicy: "cache-first", + fetchPolicy: "cache-only", notifyOnNetworkStatusChange: false, }); const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - } + expect(timesFired).toBe(1); await observable.setOptions({ query, fetchPolicy: "standby" }); - // make sure the query didn't get fired again. - expect(timesFired).toBe(1); + // make sure the query didn't get fired again. await expect(stream).not.toEmitAnything(); + expect(timesFired).toBe(1); }); it("returns a promise which eventually returns data", async () => { - const observable = mockWatchQuery( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables }, - result: { data: dataTwo }, - } - ); - + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables }, + result: { data: dataTwo }, + }, + ]), + }); + const observable = client.watchQuery({ query, variables }); const stream = new ObservableStream(observable); - const { data } = await stream.takeNext(); - - expect(data).toEqual(dataOne); + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); const res = await observable.setOptions({ fetchPolicy: "cache-and-network", }); - expect(res.data).toEqual(dataTwo); + expect(res).toEqualApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: true, + networkStatus: NetworkStatus.loading, + }); + + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); }); }); describe("setVariables", () => { it("reruns query if the variables change", async () => { - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables: differentVariables }, - result: { data: dataTwo }, - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables: differentVariables }, + result: { data: dataTwo }, + }, + ]), + }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query, variables, notifyOnNetworkStatusChange: true, @@ -779,68 +814,78 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(false); - expect(result.data).toEqual(dataOne); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.setVariables(differentVariables); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.setVariables); - } - - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + // TODO: Fix this error + // @ts-expect-error `ApolloQueryResult` needs to be updated to allow for `undefined` and this value needs to emit undefined instead of empty object + data: {}, + loading: true, + networkStatus: NetworkStatus.setVariables, + partial: true, + }); - expect(result.loading).toBe(false); - expect(result.data).toEqual(dataTwo); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); it("does invalidate the currentResult data if the variables change", async () => { - const observable = mockWatchQuery( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables: differentVariables }, - result: { data: dataTwo }, - delay: 25, - } - ); - + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables: differentVariables }, + result: { data: dataTwo }, + delay: 25, + }, + ]), + }); + const observable = client.watchQuery({ query, variables }); const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.data).toEqual(dataOne); - expect(observable.getCurrentResult().data).toEqual(dataOne); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.setVariables(differentVariables); - { - const result = await stream.takeNext(); - - expect(result.loading).toEqual(false); - expect(result.data).toEqual(dataTwo); - expect(observable.getCurrentResult().data).toEqual(dataTwo); - expect(observable.getCurrentResult().loading).toBe(false); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); + // TODO: Determine how this test differs from the previous one it("does invalidate the currentResult data if the variables change", async () => { // Standard data for all these tests const query = gql` @@ -875,53 +920,66 @@ describe("ObservableQuery", () => { ], }; - const observable: ObservableQuery = mockWatchQuery( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables: differentVariables }, - result: { data: dataTwo }, - delay: 25, - } - ); - + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables: differentVariables }, + result: { data: dataTwo }, + delay: 25, + }, + ]), + }); + const observable = client.watchQuery({ query, variables }); const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.data).toEqual(dataOne); - expect(observable.getCurrentResult().data).toEqual(dataOne); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.setVariables(differentVariables); - { - const result = await stream.takeNext(); - - expect(result.data).toEqual(dataTwo); - expect(observable.getCurrentResult().data).toEqual(dataTwo); - expect(observable.getCurrentResult().loading).toBe(false); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); it("does not invalidate the currentResult errors if the variables change", async () => { - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { errors: [error] }, - }, - { - request: { query, variables: differentVariables }, - result: { data: dataTwo }, - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { errors: [error] }, + }, + { + request: { query, variables: differentVariables }, + result: { data: dataTwo }, + }, + ]), + }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query, variables, errorPolicy: "all", @@ -929,31 +987,48 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.errors).toEqual([error]); - expect(observable.getCurrentResult().errors).toEqual([error]); - } + await expect(stream).toEmitApolloQueryResult({ + // TODO: Fix this error + // @ts-expect-error Need to update ApolloQueryResult type to allow for undefined + data: undefined, + errors: [error], + loading: false, + networkStatus: NetworkStatus.error, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + // TODO: Fix this error + // @ts-expect-error Need to update ApolloQueryResult type to allow for undefined + data: undefined, + errors: [error], + loading: false, + networkStatus: NetworkStatus.ready, + // TODO: This is not present on the emitted result so this should match + partial: true, + }); await observable.setVariables(differentVariables); - expect(observable.getCurrentResult().errors).toBeUndefined(); - - { - const result = await stream.takeNext(); - expect(result.data).toEqual(dataTwo); - expect(observable.getCurrentResult().data).toEqual(dataTwo); - expect(observable.getCurrentResult().loading).toBe(false); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); it("does not perform a query when unsubscribed if variables change", async () => { // Note: no responses, will throw if a query is made - const queryManager = mockQueryManager(); - const observable = queryManager.watchQuery({ query, variables }); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([]), + }); + const observable = client.watchQuery({ query, variables }); await observable.setVariables(differentVariables); }); @@ -970,9 +1045,12 @@ describe("ObservableQuery", () => { }, ]; - const queryManager = mockQueryManager(...mockedResponses); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink(mockedResponses), + }); const firstRequest = mockedResponses[0].request; - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query: firstRequest.query, variables: firstRequest.variables, notifyOnNetworkStatusChange: true, @@ -980,30 +1058,28 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(false); - expect(result.data).toEqual(dataOne); - expect(result.networkStatus).toBe(NetworkStatus.ready); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.setVariables(differentVariables); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.setVariables); - } - - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + // TODO: Fix this error + // @ts-expect-error Ensure ApolloQueryResult allows for undefined and fix this value to match + data: {}, + loading: true, + networkStatus: NetworkStatus.setVariables, + partial: true, + }); - expect(result.loading).toBe(false); - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(result.data).toEqual(dataTwo); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); @@ -1020,9 +1096,12 @@ describe("ObservableQuery", () => { }, ]; - const queryManager = mockQueryManager(...mockedResponses); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink(mockedResponses), + }); const firstRequest = mockedResponses[0].request; - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query: firstRequest.query, variables: firstRequest.variables, notifyOnNetworkStatusChange: true, @@ -1030,51 +1109,54 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(false); - expect(result.data).toEqual(dataOne); - expect(result.networkStatus).toBe(NetworkStatus.ready); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.refetch(differentVariables); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.setVariables); - } - - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + // TODO: Fix this error + // @ts-expect-error Need to update ApolloQueryResult to allow undefined and fix this value + data: {}, + loading: true, + networkStatus: NetworkStatus.setVariables, + partial: true, + }); - expect(result.loading).toBe(false); - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(result.data).toEqual(dataTwo); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); it("does not rerun query if variables do not change", async () => { - const observable = mockWatchQuery( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables }, - result: { data: dataTwo }, - } - ); - + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables }, + result: { data: dataTwo }, + }, + ]), + }); + const observable = client.watchQuery({ query, variables }); const stream = new ObservableStream(observable); - const result = await stream.takeNext(); - - expect(result.data).toEqual(dataOne); + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.setVariables(variables); @@ -1082,31 +1164,34 @@ describe("ObservableQuery", () => { }); it("handles variables changing while a query is in-flight", async () => { + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + delay: 20, + }, + { + request: { query, variables: differentVariables }, + result: { data: dataTwo }, + delay: 20, + }, + ]), + }); // The expected behavior is that the original variables are forgotten // and the query stays in loading state until the result for the new variables // has returned. - const observable = mockWatchQuery( - { - request: { query, variables }, - result: { data: dataOne }, - delay: 20, - }, - { - request: { query, variables: differentVariables }, - result: { data: dataTwo }, - delay: 20, - } - ); - + const observable = client.watchQuery({ query, variables }); const stream = new ObservableStream(observable); await observable.setVariables(differentVariables); - const result = await stream.takeNext(); - - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(result.loading).toBe(false); - expect(result.data).toEqual(dataTwo); + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); @@ -1125,71 +1210,66 @@ describe("ObservableQuery", () => { }, ]; - const queryManager = mockQueryManager(...mockedResponses); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink(mockedResponses), + }); const firstRequest = mockedResponses[0].request; - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query: firstRequest.query, variables: firstRequest.variables, fetchPolicy: "cache-first", }); - const mocks = mockFetchQuery(queryManager); + // TODO: Determine if we can test this without reaching into internal + // implementation details + const mocks = mockFetchQuery(client["queryManager"]); const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result).toEqual({ - loading: false, - networkStatus: NetworkStatus.ready, - data: dataOne, - }); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observable.refetch(differentVariables); - { - const result = await stream.takeNext(); - - expect(result).toEqual({ - loading: false, - networkStatus: NetworkStatus.ready, - data: dataTwo, - }); + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); - const fqbpCalls = mocks.fetchQueryByPolicy.mock.calls; - expect(fqbpCalls.length).toBe(2); - expect(fqbpCalls[0][1].fetchPolicy).toEqual("cache-first"); - expect(fqbpCalls[1][1].fetchPolicy).toEqual("network-only"); + const fqbpCalls = mocks.fetchQueryByPolicy.mock.calls; + expect(fqbpCalls.length).toBe(2); + expect(fqbpCalls[0][1].fetchPolicy).toEqual("cache-first"); + expect(fqbpCalls[1][1].fetchPolicy).toEqual("network-only"); - const fqoCalls = mocks.fetchConcastWithInfo.mock.calls; - expect(fqoCalls.length).toBe(2); - expect(fqoCalls[0][1].fetchPolicy).toEqual("cache-first"); - expect(fqoCalls[1][1].fetchPolicy).toEqual("network-only"); + const fqoCalls = mocks.fetchConcastWithInfo.mock.calls; + expect(fqoCalls.length).toBe(2); + expect(fqoCalls[0][1].fetchPolicy).toEqual("cache-first"); + expect(fqoCalls[1][1].fetchPolicy).toEqual("network-only"); - // Although the options.fetchPolicy we passed just now to - // fetchQueryByPolicy should have been network-only, - // observable.options.fetchPolicy should now be updated to - // cache-first, thanks to options.nextFetchPolicy. - expect(observable.options.fetchPolicy).toBe("cache-first"); - } + // Although the options.fetchPolicy we passed just now to + // fetchQueryByPolicy should have been network-only, + // observable.options.fetchPolicy should now be updated to + // cache-first, thanks to options.nextFetchPolicy. + expect(observable.options.fetchPolicy).toBe("cache-first"); await expect(stream).not.toEmitAnything(); }); it("calling refetch with different variables before the query itself resolved will only yield the result for the new variables", async () => { const observers: SubscriptionObserver>[] = []; - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache(), - link: new ApolloLink((operation, forward) => { - return new Observable((observer) => { - observers.push(observer); - }); - }), - }) - ); - const observableQuery = queryManager.watchQuery({ + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink((operation, forward) => { + return new Observable((observer) => { + observers.push(observer); + }); + }), + }); + const observableQuery = client.watchQuery({ query, variables: { id: 1 }, }); @@ -1203,12 +1283,10 @@ describe("ObservableQuery", () => { observers[1].next({ data: dataTwo }); observers[1].complete(); - const result = await stream.takeNext(); - - expect(result).toEqual({ + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, loading: false, networkStatus: NetworkStatus.ready, - data: dataTwo, }); await expect(stream).not.toEmitAnything(); @@ -1216,17 +1294,15 @@ describe("ObservableQuery", () => { it("calling refetch multiple times with different variables will return only results for the most recent variables", async () => { const observers: SubscriptionObserver>[] = []; - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache(), - link: new ApolloLink((operation, forward) => { - return new Observable((observer) => { - observers.push(observer); - }); - }), - }) - ); - const observableQuery = queryManager.watchQuery({ + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink((operation, forward) => { + return new Observable((observer) => { + observers.push(observer); + }); + }), + }); + const observableQuery = client.watchQuery({ query, variables: { id: 1 }, }); @@ -1235,14 +1311,11 @@ describe("ObservableQuery", () => { observers[0].next({ data: dataOne }); observers[0].complete(); - { - const result = await stream.takeNext(); - expect(result).toEqual({ - loading: false, - networkStatus: NetworkStatus.ready, - data: dataOne, - }); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); void observableQuery.refetch({ id: 2 }); void observableQuery.refetch({ id: 3 }); @@ -1259,18 +1332,17 @@ describe("ObservableQuery", () => { }); observers[2].complete(); - { - const result = await stream.takeNext(); - expect(result).toEqual({ - loading: false, - networkStatus: NetworkStatus.ready, - data: { - people_one: { - name: "SomeOneElse", - }, + await expect(stream).toEmitApolloQueryResult({ + data: { + people_one: { + name: "SomeOneElse", }, - }); - } + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); }); it("calls fetchRequest with fetchPolicy `no-cache` when using `no-cache` fetch policy", async () => { @@ -1285,15 +1357,20 @@ describe("ObservableQuery", () => { }, ]; - const queryManager = mockQueryManager(...mockedResponses); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink(mockedResponses), + }); const firstRequest = mockedResponses[0].request; - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query: firstRequest.query, variables: firstRequest.variables, fetchPolicy: "no-cache", }); - const mocks = mockFetchQuery(queryManager); + // TODO: Determine how we can test this without looking at internal + // implementation details + const mocks = mockFetchQuery(client["queryManager"]); const stream = new ObservableStream(observable); await stream.takeNext(); @@ -1330,31 +1407,34 @@ describe("ObservableQuery", () => { const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } }; const variables2 = { first: 1 }; - const queryManager = mockQueryManager( - { - request: { - query: queryWithVars, - variables: variables1, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { + query: queryWithVars, + variables: variables1, + }, + result: { data }, }, - result: { data }, - }, - { - request: { - query: queryWithVars, - variables: variables2, + { + request: { + query: queryWithVars, + variables: variables2, + }, + result: { data: data2 }, }, - result: { data: data2 }, - }, - { - request: { - query: queryWithVars, - variables: variables1, + { + request: { + query: queryWithVars, + variables: variables1, + }, + result: { data }, }, - result: { data }, - } - ); + ]), + }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query: queryWithVars, variables: variables1, fetchPolicy: "cache-and-network", @@ -1363,42 +1443,42 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.data).toEqual(data); - expect(result.loading).toBe(false); - await observable.refetch(variables2); - } - - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.setVariables); - } + await observable.refetch(variables2); - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data: {}, + loading: true, + networkStatus: NetworkStatus.setVariables, + partial: true, + }); - expect(result.data).toEqual(data2); - expect(result.loading).toBe(false); - await observable.refetch(variables1); - } + await expect(stream).toEmitApolloQueryResult({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); - { - const result = await stream.takeNext(); + await observable.refetch(variables1); - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.setVariables); - } + await expect(stream).toEmitApolloQueryResult({ + data, + loading: true, + networkStatus: NetworkStatus.setVariables, + }); - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); - expect(result.data).toEqual(data); - expect(result.loading).toBe(false); - } + await expect(stream).not.toEmitAnything(); }); it("resets fetchPolicy when variables change when using nextFetchPolicy", async () => { @@ -1419,39 +1499,42 @@ describe("ObservableQuery", () => { const data2 = { allPeople: { people: [{ name: "Leia Skywalker" }] } }; const variables2 = { first: 1 }; - const queryManager = mockQueryManager( - { - request: { - query: queryWithVars, - variables: variables1, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { + query: queryWithVars, + variables: variables1, + }, + result: { data }, }, - result: { data }, - }, - { - request: { - query: queryWithVars, - variables: variables2, + { + request: { + query: queryWithVars, + variables: variables2, + }, + result: { data: data2 }, }, - result: { data: data2 }, - }, - { - request: { - query: queryWithVars, - variables: variables1, + { + request: { + query: queryWithVars, + variables: variables1, + }, + result: { data }, }, - result: { data }, - }, - { - request: { - query: queryWithVars, - variables: variables2, + { + request: { + query: queryWithVars, + variables: variables2, + }, + result: { data: data2 }, }, - result: { data: data2 }, - } - ); + ]), + }); const usedFetchPolicies: WatchQueryFetchPolicy[] = []; - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query: queryWithVars, variables: variables1, fetchPolicy: "cache-and-network", @@ -1473,85 +1556,80 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result.data).toEqual(data); - expect(result.loading).toBe(false); - expect(result.error).toBeUndefined(); - expect(observable.options.fetchPolicy).toBe("cache-first"); - } + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.options.fetchPolicy).toBe("cache-first"); await observable.refetch(variables2); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.setVariables); - expect(result.error).toBeUndefined(); - expect(observable.options.fetchPolicy).toBe("cache-first"); - } - - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data: {}, + loading: true, + networkStatus: NetworkStatus.setVariables, + partial: true, + }); + expect(observable.options.fetchPolicy).toBe("cache-first"); - expect(result.data).toEqual(data2); - expect(result.loading).toBe(false); - expect(result.error).toBeUndefined(); - expect(observable.options.fetchPolicy).toBe("cache-first"); - } + await expect(stream).toEmitApolloQueryResult({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.options.fetchPolicy).toBe("cache-first"); { const result = await observable.setOptions({ variables: variables1 }); - expect(result.data).toEqual(data); - expect(observable.options.fetchPolicy).toBe("cache-first"); - } - - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.setVariables); - expect(result.error).toBeUndefined(); - expect(observable.options.fetchPolicy).toBe("cache-first"); - } - - { - const result = await stream.takeNext(); - - expect(result.data).toEqual(data); - expect(result.loading).toBe(false); - expect(result.error).toBeUndefined(); - expect(observable.options.fetchPolicy).toBe("cache-first"); - } - - { - const result = await observable.reobserve({ variables: variables2 }); - - expect(result.data).toEqual(data2); + expect(result).toEqualApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); expect(observable.options.fetchPolicy).toBe("cache-first"); } - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data, + loading: true, + networkStatus: NetworkStatus.setVariables, + }); + expect(observable.options.fetchPolicy).toBe("cache-first"); - expect(result.data).toEqual(data2); - expect(result.loading).toBe(true); - expect(result.error).toBeUndefined(); - expect(observable.options.fetchPolicy).toBe("cache-first"); - } + await expect(stream).toEmitApolloQueryResult({ + data, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.options.fetchPolicy).toBe("cache-first"); { - const result = await stream.takeNext(); + const result = await observable.reobserve({ variables: variables2 }); - expect(result.data).toEqual(data2); - expect(result.loading).toBe(false); - expect(result.error).toBeUndefined(); + expect(result).toEqualApolloQueryResult({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); expect(observable.options.fetchPolicy).toBe("cache-first"); } + await expect(stream).toEmitApolloQueryResult({ + data: data2, + loading: true, + networkStatus: NetworkStatus.setVariables, + }); + expect(observable.options.fetchPolicy).toBe("cache-first"); + + await expect(stream).toEmitApolloQueryResult({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.options.fetchPolicy).toBe("cache-first"); + expect(usedFetchPolicies).toEqual([ "cache-and-network", "network-only", @@ -1608,31 +1686,18 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result).toEqual({ - data: { - counter: 1, - }, - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - } - - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data: { counter: 1 }, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); - expect(result).toEqual({ - data: { - counter: 2, - name: "Ben", - }, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } + await expect(stream).toEmitApolloQueryResult({ + data: { counter: 2, name: "Ben" }, + loading: false, + networkStatus: NetworkStatus.ready, + }); const oldLinkObs = linkObservable; // Make the next network request fail. @@ -1642,40 +1707,27 @@ describe("ObservableQuery", () => { intentionalNetworkFailure ); - { - const result = await stream.takeNext(); - - expect(result).toEqual({ - data: { - counter: 3, - name: "Ben", - }, - loading: true, - networkStatus: NetworkStatus.refetch, - }); - } - - { - const error = await stream.takeError(); + await expect(stream).toEmitApolloQueryResult({ + data: { counter: 3, name: "Ben" }, + loading: true, + networkStatus: NetworkStatus.refetch, + }); - expect(error).toBe(intentionalNetworkFailure); - } + await expect(stream).toEmitError(intentionalNetworkFailure); // Switch back from errorObservable. linkObservable = oldLinkObs; - { - const result = await observable.refetch(); + const result = await observable.refetch(); - expect(result).toEqual({ - data: { - counter: 5, - name: "Ben", - }, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } + expect(result).toEqualApolloQueryResult({ + data: { + counter: 5, + name: "Ben", + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); @@ -1716,53 +1768,53 @@ describe("ObservableQuery", () => { }; } - const observableWithoutVariables = mockWatchQuery( - makeMock("a", "b", "c"), - makeMock("d", "e") - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([makeMock("a", "b", "c"), makeMock("d", "e")]), + }); + const observableWithoutVariables = client.watchQuery({ + query: queryWithoutVariables, + variables: { variables: ["a", "b", "c"] }, + }); const stream = new ObservableStream(observableWithoutVariables); - { - const result = await stream.takeNext(); - - expect(result.error).toBeUndefined(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ + await expect(stream).toEmitApolloQueryResult({ + data: { getVars: [ { __typename: "Var", name: "a" }, { __typename: "Var", name: "b" }, { __typename: "Var", name: "c" }, ], - }); - } + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observableWithoutVariables.refetch({ variables: ["d", "e"], }); - { - const result = await stream.takeNext(); - - expect(result.error).toBeUndefined(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ + await expect(stream).toEmitApolloQueryResult({ + data: { getVars: [ { __typename: "Var", name: "d" }, { __typename: "Var", name: "e" }, ], - }); + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith( - [ - "Called refetch(%o) for query %o, which does not declare a $variables variable.", - "Did you mean to call refetch(variables) instead of refetch({ variables })?", - ].join("\n"), - { variables: ["d", "e"] }, - "QueryWithoutVariables" - ); - } + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + [ + "Called refetch(%o) for query %o, which does not declare a $variables variable.", + "Did you mean to call refetch(variables) instead of refetch({ variables })?", + ].join("\n"), + { variables: ["d", "e"] }, + "QueryWithoutVariables" + ); await expect(stream).not.toEmitAnything(); }); @@ -1800,23 +1852,14 @@ describe("ObservableQuery", () => { }; } - // We construct the queryManager manually here rather than using - // `mockWatchQuery` because we need to silence console warnings for - // unmatched variables since. This test checks for calls to - // `console.warn` and unfortunately `mockSingleLink` (used by - // `mockWatchQuery`) does not support the ability to disable warnings - // without introducing a breaking change. Instead we construct this - // manually to be able to turn off warnings for this test. const mocks = [makeMock("a", "b", "c"), makeMock("d", "e")]; const firstRequest = mocks[0].request; - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link: new MockLink(mocks, true, { showWarnings: false }), - }) - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink(mocks, true, { showWarnings: false }), + }); - const observableWithVarsVar = queryManager.watchQuery({ + const observableWithVarsVar = client.watchQuery({ query: firstRequest.query, variables: firstRequest.variables, notifyOnNetworkStatusChange: false, @@ -1824,19 +1867,17 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observableWithVarsVar); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(false); - expect(result.error).toBeUndefined(); - expect(result.data).toEqual({ + await expect(stream).toEmitApolloQueryResult({ + data: { getVars: [ { __typename: "Var", name: "a" }, { __typename: "Var", name: "b" }, { __typename: "Var", name: "c" }, ], - }); - } + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); // It's a common mistake to call refetch({ variables }) when you meant // to call refetch(variables). @@ -1845,13 +1886,13 @@ describe("ObservableQuery", () => { variables: { vars: ["d", "e"] }, }); - { - const error = await stream.takeError(); - - expect(error.message).toMatch( - "No more mocked responses for the query: query QueryWithVarsVar($vars: [String!])" - ); - } + await expect(stream).toEmitError( + expect.objectContaining({ + message: expect.stringMatching( + /No more mocked responses for the query: query QueryWithVarsVar\(\$vars: \[String!\]\)/ + ), + }) + ); await expect(promise).rejects.toEqual( expect.objectContaining({ @@ -1872,6 +1913,7 @@ describe("ObservableQuery", () => { await expect(stream).not.toEmitAnything(); }); + it("should not warn if passed { variables } and query declares $variables", async () => { using _ = spyOnConsole("warn"); @@ -1907,43 +1949,44 @@ describe("ObservableQuery", () => { }; } - const observableWithVariablesVar = mockWatchQuery( - makeMock("a", "b", "c"), - makeMock("d", "e") - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([makeMock("a", "b", "c"), makeMock("d", "e")]), + }); - const stream = new ObservableStream(observableWithVariablesVar); + const observableWithVariablesVar = client.watchQuery({ + query: queryWithVariablesVar, + variables: { variables: ["a", "b", "c"] }, + }); - { - const result = await stream.takeNext(); + const stream = new ObservableStream(observableWithVariablesVar); - expect(result.loading).toBe(false); - expect(result.error).toBeUndefined(); - expect(result.data).toEqual({ + await expect(stream).toEmitApolloQueryResult({ + data: { getVars: [ { __typename: "Var", name: "a" }, { __typename: "Var", name: "b" }, { __typename: "Var", name: "c" }, ], - }); - } + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); await observableWithVariablesVar.refetch({ variables: ["d", "e"] }); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(false); - expect(result.error).toBeUndefined(); - expect(result.data).toEqual({ + await expect(stream).toEmitApolloQueryResult({ + data: { getVars: [ { __typename: "Var", name: "d" }, { __typename: "Var", name: "e" }, ], - }); + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); - expect(console.warn).not.toHaveBeenCalled(); - } + expect(console.warn).not.toHaveBeenCalled(); await expect(stream).not.toEmitAnything(); }); @@ -2018,19 +2061,17 @@ describe("ObservableQuery", () => { pets: petData.slice(0, 3), }; - const ni = mockSingleLink( - { - request: { query: queryWithFragment, variables }, - result: { data: dataOneWithTypename }, - }, - { - request: { query: queryWithFragment, variables }, - result: { data: dataTwoWithTypename }, - } - ); - const client = new ApolloClient({ - link: ni, + link: new MockLink([ + { + request: { query: queryWithFragment, variables }, + result: { data: dataOneWithTypename }, + }, + { + request: { query: queryWithFragment, variables }, + result: { data: dataTwoWithTypename }, + }, + ]), cache: new InMemoryCache({ possibleTypes: { Creature: ["Pet"], @@ -2047,65 +2088,80 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data: dataOneWithTypename, + loading: false, + networkStatus: NetworkStatus.ready, + }); - expect(result.loading).toBe(false); - expect(result.networkStatus).toEqual(NetworkStatus.ready); - expect(result.data).toEqual(dataOneWithTypename); - expect(observable.getCurrentResult()).toEqual(result); - } + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: dataOneWithTypename, + loading: false, + networkStatus: NetworkStatus.ready, + }); void observable.refetch(); - { - const result = await stream.takeNext(); - - expect(result.loading).toBe(true); - expect(result.networkStatus).toEqual(NetworkStatus.refetch); - expect(observable.getCurrentResult()).toEqual(result); - } - - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data: dataOneWithTypename, + loading: true, + networkStatus: NetworkStatus.refetch, + }); + expect(observable.getCurrentResult()).toEqual({ + data: dataOneWithTypename, + loading: true, + networkStatus: NetworkStatus.refetch, + }); - expect(result.loading).toBe(false); - expect(result.networkStatus).toEqual(NetworkStatus.ready); - expect(result.data).toEqual(dataTwoWithTypename); - expect(observable.getCurrentResult()).toEqual(result); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataTwoWithTypename, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult()).toEqual({ + data: dataTwoWithTypename, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); it("returns the current query status immediately", async () => { - const observable = mockWatchQuery({ - request: { query, variables }, - result: { data: dataOne }, - delay: 100, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + delay: 100, + }, + ]), }); - + const observable = client.watchQuery({ query, variables }); const stream = new ObservableStream(observable); - expect(observable.getCurrentResult()).toEqual({ + // TODO: Fix this error + // @ts-expect-error ApolloQueryResult expects a `data` property, but the value returned from `getCurrentResult` does not include it + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ loading: true, - data: undefined, - networkStatus: 1, + networkStatus: NetworkStatus.loading, partial: true, }); await tick(); - expect(observable.getCurrentResult()).toEqual({ + // TODO: Fix this error + // @ts-expect-error ApolloQueryResult expects a `data` property, but the value returned from `getCurrentResult` does not include it + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ loading: true, - data: undefined, - networkStatus: 1, + networkStatus: NetworkStatus.loading, partial: true, }); await stream.takeNext(); - expect(observable.getCurrentResult()).toEqual({ + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ data: dataOne, loading: false, networkStatus: 7, @@ -2113,22 +2169,27 @@ describe("ObservableQuery", () => { }); it("returns results from the store immediately", async () => { - const queryManager = mockQueryManager({ - request: { query, variables }, - result: { data: dataOne }, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + ]), }); - const result = await queryManager.query({ query, variables }); + const result = await client.query({ query, variables }); - expect(result).toEqual({ + expect(result).toEqualApolloQueryResult({ data: dataOne, loading: false, networkStatus: 7, }); - const observable = queryManager.watchQuery({ query, variables }); + const observable = client.watchQuery({ query, variables }); - expect(observable.getCurrentResult()).toEqual({ + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ data: dataOne, loading: false, networkStatus: NetworkStatus.ready, @@ -2136,29 +2197,45 @@ describe("ObservableQuery", () => { }); it("returns errors from the store immediately", async () => { - const queryManager = mockQueryManager({ - request: { query, variables }, - result: { errors: [error] }, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { errors: [error] }, + }, + ]), }); - const observable = queryManager.watchQuery({ query, variables }); + const observable = client.watchQuery({ query, variables }); const stream = new ObservableStream(observable); - const theError = await stream.takeError(); - const currentResult = observable.getCurrentResult(); - - expect(theError.graphQLErrors).toEqual([error]); - expect(currentResult.loading).toBe(false); - expect(currentResult.error!.graphQLErrors).toEqual([error]); + await expect(stream).toEmitError( + new ApolloError({ graphQLErrors: [error] }) + ); + // TODO: Fix this error + // @ts-expect-error ApolloQueryResult expects `data` property to be defined but it is not returned here + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + error: new ApolloError({ graphQLErrors: [error] }), + errors: [error], + loading: false, + networkStatus: NetworkStatus.error, + partial: true, + }); }); it("returns referentially equal errors", async () => { - const queryManager = mockQueryManager({ - request: { query, variables }, - result: { errors: [error] }, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { errors: [error] }, + }, + ]), }); - const observable = queryManager.watchQuery({ query, variables }); + const observable = client.watchQuery({ query, variables }); await expect(observable.result()).rejects.toThrow( new ApolloError({ graphQLErrors: [error] }) @@ -2167,18 +2244,30 @@ describe("ObservableQuery", () => { const currentResult = observable.getCurrentResult(); const currentResult2 = observable.getCurrentResult(); - expect(currentResult.loading).toBe(false); - expect(currentResult.error!.graphQLErrors).toEqual([error]); + // @ts-expect-error ApolloQueryResult expects a `data` property to be returned + expect(currentResult).toEqualApolloQueryResult({ + error: new ApolloError({ graphQLErrors: [error] }), + errors: [error], + loading: false, + networkStatus: NetworkStatus.error, + partial: true, + }); + expect(currentResult.error === currentResult2.error).toBe(true); }); it("returns errors with data if errorPolicy is all", async () => { - const queryManager = mockQueryManager({ - request: { query, variables }, - result: { data: dataOne, errors: [error] }, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne, errors: [error] }, + }, + ]), }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query, variables, errorPolicy: "all", @@ -2187,20 +2276,34 @@ describe("ObservableQuery", () => { const result = await observable.result(); const currentResult = observable.getCurrentResult(); - expect(result.data).toEqual(dataOne); - expect(result.errors).toEqual([error]); - expect(currentResult.loading).toBe(false); - expect(currentResult.errors).toEqual([error]); - expect(currentResult.error).toBeUndefined(); + expect(result).toEqualApolloQueryResult({ + data: dataOne, + errors: [error], + loading: false, + networkStatus: NetworkStatus.error, + }); + expect(currentResult).toEqualApolloQueryResult({ + data: dataOne, + errors: [error], + loading: false, + // TODO: The networkStatus returned here is different than the one + // returned from `observable.result()`. These should match + networkStatus: NetworkStatus.ready, + }); }); it("errors out if errorPolicy is none", async () => { - const queryManager = mockQueryManager({ - request: { query, variables }, - result: { data: dataOne, errors: [error] }, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne, errors: [error] }, + }, + ]), }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query, variables, errorPolicy: "none", @@ -2212,12 +2315,17 @@ describe("ObservableQuery", () => { }); it("errors out if errorPolicy is none and the observable has completed", async () => { - const queryManager = mockQueryManager({ - request: { query, variables }, - result: { data: dataOne, errors: [error] }, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne, errors: [error] }, + }, + ]), }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query, variables, errorPolicy: "none", @@ -2230,12 +2338,17 @@ describe("ObservableQuery", () => { }); it("ignores errors with data if errorPolicy is ignore", async () => { - const queryManager = mockQueryManager({ - request: { query, variables }, - result: { errors: [error], data: dataOne }, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { errors: [error], data: dataOne }, + }, + ]), }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query, variables, errorPolicy: "ignore", @@ -2244,11 +2357,16 @@ describe("ObservableQuery", () => { const result = await observable.result(); const currentResult = observable.getCurrentResult(); - expect(result.data).toEqual(dataOne); - expect(result.errors).toBeUndefined(); - expect(currentResult.loading).toBe(false); - expect(currentResult.errors).toBeUndefined(); - expect(currentResult.error).toBeUndefined(); + expect(result).toEqualApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(currentResult).toEqualApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); it("returns partial data from the store immediately", async () => { @@ -2268,178 +2386,175 @@ describe("ObservableQuery", () => { }, }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query: superQuery, variables }, - result: { data: superDataOne }, - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query: superQuery, variables }, + result: { data: superDataOne }, + }, + ]), + }); - await queryManager.query({ query, variables }); + await client.query({ query, variables }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query: superQuery, variables, returnPartialData: true, }); - expect(observable.getCurrentResult()).toEqual({ + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ data: dataOne, loading: true, - networkStatus: 1, + networkStatus: NetworkStatus.loading, partial: true, }); const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - const current = observable.getCurrentResult(); - - expect(result).toEqual({ - data: dataOne, - loading: true, - networkStatus: 1, - partial: true, - }); - expect(current.data).toEqual(dataOne); - expect(current.loading).toEqual(true); - expect(current.networkStatus).toEqual(1); - } - - { - const result = await stream.takeNext(); - const current = observable.getCurrentResult(); + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: dataOne, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); - expect(result).toEqual({ - data: superDataOne, - loading: false, - networkStatus: 7, - }); - expect(current.data).toEqual(superDataOne); - expect(current.loading).toEqual(false); - expect(current.networkStatus).toEqual(7); - } + await expect(stream).toEmitApolloQueryResult({ + data: superDataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: superDataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); it("returns loading even if full data is available when using network-only fetchPolicy", async () => { - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables }, - result: { data: dataTwo }, - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables }, + result: { data: dataTwo }, + }, + ]), + }); - const result = await queryManager.query({ query, variables }); + const result = await client.query({ query, variables }); - expect(result).toEqual({ + expect(result).toEqualApolloQueryResult({ data: dataOne, loading: false, networkStatus: NetworkStatus.ready, }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query, variables, fetchPolicy: "network-only", }); - expect(observable.getCurrentResult()).toEqual({ - data: undefined, + // TODO: Fix this issue + // @ts-expect-error `ApolloQueryResult` expects a `data` property + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ loading: true, networkStatus: NetworkStatus.loading, }); const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result).toEqual({ - loading: true, - data: undefined, - networkStatus: NetworkStatus.loading, - }); - } - - { - const result = await stream.takeNext(); + // TODO: Fix this issue + // @ts-expect-error `ApolloQueryResult` expects a `data` property + await expect(stream).toEmitApolloQueryResult({ + loading: true, + networkStatus: NetworkStatus.loading, + }); - expect(result).toEqual({ - data: dataTwo, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); it("returns loading on no-cache fetchPolicy queries when calling getCurrentResult", async () => { - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query, variables }, - result: { data: dataTwo }, - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query, variables }, + result: { data: dataTwo }, + }, + ]), + }); - await queryManager.query({ query, variables }); + await client.query({ query, variables }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query, variables, fetchPolicy: "no-cache", }); - expect(observable.getCurrentResult()).toEqual({ - data: undefined, + // TODO: Fix this issue + // @ts-expect-error `ApolloQueryResult` expects a `data` property + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ loading: true, - networkStatus: 1, + networkStatus: NetworkStatus.loading, }); const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - const current = observable.getCurrentResult(); - - expect(result).toEqual({ - data: undefined, - loading: true, - networkStatus: NetworkStatus.loading, - }); - expect(current.data).toBeUndefined(); - expect(current.loading).toBe(true); - expect(current.networkStatus).toBe(NetworkStatus.loading); - } + // TODO: Fix this issue + // @ts-expect-error `ApolloQueryResult` expects a `data` property + await expect(stream).toEmitApolloQueryResult({ + loading: true, + networkStatus: NetworkStatus.loading, + }); + // TODO: Fix this issue + // @ts-expect-error `ApolloQueryResult` expects a `data` property + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + loading: true, + networkStatus: NetworkStatus.loading, + }); - { - const result = await stream.takeNext(); - const current = observable.getCurrentResult(); + await expect(stream).toEmitApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); - expect(result).toEqual({ - data: dataTwo, - loading: false, - networkStatus: NetworkStatus.ready, - }); - expect(current.data).toEqual(dataTwo); - expect(current.loading).toBe(false); - expect(current.networkStatus).toBe(NetworkStatus.ready); - } + await expect(stream).not.toEmitAnything(); }); it("handles multiple calls to getCurrentResult without losing data", async () => { @@ -2478,28 +2593,29 @@ describe("ObservableQuery", () => { }, }); - { - const result = await stream.takeNext(); - expect(result.data).toEqual({ + await expect(stream).toEmitApolloQueryResult({ + data: { greeting: { message: "Hello world", __typename: "Greeting", }, - }); - } - - expect(obs.getCurrentResult().data).toEqual({ - greeting: { - message: "Hello world", - __typename: "Greeting", }, + loading: false, + networkStatus: NetworkStatus.ready, }); - expect(obs.getCurrentResult().data).toEqual({ - greeting: { - message: "Hello world", - __typename: "Greeting", + expect(obs.getCurrentResult()).toEqualApolloQueryResult({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, }, + loading: false, + networkStatus: NetworkStatus.ready, + // TODO: This should not be there since the observable did not emit this + // property. + partial: true, }); link.simulateResult( @@ -2523,9 +2639,8 @@ describe("ObservableQuery", () => { true ); - { - const result = await stream.takeNext(); - expect(result.data).toEqual({ + await expect(stream).toEmitApolloQueryResult({ + data: { greeting: { message: "Hello world", recipient: { @@ -2534,30 +2649,44 @@ describe("ObservableQuery", () => { }, __typename: "Greeting", }, - }); - } + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); - expect(obs.getCurrentResult().data).toEqual({ - greeting: { - message: "Hello world", - recipient: { - name: "Alice", - __typename: "Person", + expect(obs.getCurrentResult()).toEqualApolloQueryResult({ + data: { + greeting: { + message: "Hello world", + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", }, - __typename: "Greeting", }, + loading: false, + networkStatus: NetworkStatus.ready, }); - expect(obs.getCurrentResult().data).toEqual({ - greeting: { - message: "Hello world", - recipient: { - name: "Alice", - __typename: "Person", + // This 2nd identical check is intentional to ensure calling this function + // more than once returns the right value. + expect(obs.getCurrentResult()).toEqualApolloQueryResult({ + data: { + greeting: { + message: "Hello world", + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", }, - __typename: "Greeting", }, + loading: false, + networkStatus: NetworkStatus.ready, }); + + await expect(stream).not.toEmitAnything(); }); { @@ -2790,10 +2919,8 @@ describe("ObservableQuery", () => { const cache = new InMemoryCache({}); cache.writeQuery({ query, data: cacheValues.initial }); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ link, cache }) - ); - const observableQuery = queryManager.watchQuery({ + const client = new ApolloClient({ link, cache }); + const observableQuery = client.watchQuery({ query, fetchPolicy, nextFetchPolicy, @@ -2881,54 +3008,73 @@ describe("ObservableQuery", () => { }; it("returns optimistic mutation results from the store", async () => { - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: dataOne }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + }, + ]), + }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query, variables, }); const stream = new ObservableStream(observable); - { - const result = await stream.takeNext(); - - expect(result).toEqual({ - data: dataOne, - loading: false, - networkStatus: 7, - }); - expect(observable.getCurrentResult()).toEqual(result); - } + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); - void queryManager.mutate({ + void client.mutate({ mutation, optimisticResponse, updateQueries, }); - { - const result = await stream.takeNext(); - - expect(observable.getCurrentResult()).toEqual(result); - expect(result.data.people_one).toEqual(optimisticResponse); - } - - { - const result = await stream.takeNext(); + await expect(stream).toEmitApolloQueryResult({ + data: { + people_one: optimisticResponse, + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: { + people_one: optimisticResponse, + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); - expect(observable.getCurrentResult()).toEqual(result); - expect(result.data.people_one).toEqual(mutationData); - } + await expect(stream).toEmitApolloQueryResult({ + data: { + people_one: mutationData, + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: { + people_one: mutationData, + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); @@ -2952,11 +3098,11 @@ describe("ObservableQuery", () => { }) { const cache = new InMemoryCache(); const client = new ApolloClient({ - link: mockSingleLink( + link: new MockLink([ { request: queryOptions, result: { data: { value: 1 } } }, { request: queryOptions, result: { data: { value: 2 } } }, - { request: queryOptions, result: { data: { value: 3 } } } - ).setOnError((error) => { + { request: queryOptions, result: { data: { value: 3 } } }, + ]).setOnError((error) => { throw error; }), assumeImmutableResults, @@ -3019,14 +3165,22 @@ describe("ObservableQuery", () => { }); }); + // TODO: Determine if this API is useful. This clears out internal state not + // accessible to the end user. describe("resetQueryStoreErrors", () => { it("should remove any GraphQLError's stored in the query store", async () => { const graphQLError = new GraphQLError("oh no!"); - const observable = mockWatchQuery({ - request: { query, variables }, - result: { errors: [graphQLError] }, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { errors: [graphQLError] }, + }, + ]), }); + const observable = client.watchQuery({ query, variables }); await new Promise((resolve) => { observable.subscribe({ @@ -3047,10 +3201,16 @@ describe("ObservableQuery", () => { it("should remove network error's stored in the query store", async () => { const networkError = new Error("oh no!"); - const observable = mockWatchQuery({ - request: { query, variables }, - result: { data: dataOne }, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + ]), }); + const observable = client.watchQuery({ query, variables }); const stream = new ObservableStream(observable); @@ -3191,12 +3351,17 @@ describe("ObservableQuery", () => { }); it("QueryInfo does not notify for !== but deep-equal results", async () => { - const queryManager = mockQueryManager({ - request: { query, variables }, - result: { data: dataOne }, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + ]), }); - const observable = queryManager.watchQuery({ + const observable = client.watchQuery({ query, variables, // If we let the cache return canonical results, it will be harder to @@ -3213,12 +3378,10 @@ describe("ObservableQuery", () => { const stream = new ObservableStream(observable); - const result = await stream.takeNext(); - - expect(result).toEqual({ + await expect(stream).toEmitApolloQueryResult({ + data: dataOne, loading: false, networkStatus: NetworkStatus.ready, - data: dataOne, }); let invalidateCount = 0; @@ -3258,21 +3421,27 @@ describe("ObservableQuery", () => { expect(notifySpy).not.toHaveBeenCalled(); expect(invalidateCount).toBe(1); expect(onWatchUpdatedCount).toBe(1); - queryManager.stop(); + client.stop(); await expect(stream).not.toEmitAnything(); }); it("ObservableQuery#map respects Symbol.species", async () => { - const observable = mockWatchQuery({ - request: { query, variables }, - result: { data: dataOne }, + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + ]), }); + const observable = client.watchQuery({ query, variables }); expect(observable).toBeInstanceOf(Observable); expect(observable).toBeInstanceOf(ObservableQuery); const mapped = observable.map((result) => { - expect(result).toEqual({ + expect(result).toEqualApolloQueryResult({ loading: false, networkStatus: NetworkStatus.ready, data: dataOne, @@ -3285,25 +3454,15 @@ describe("ObservableQuery", () => { expect(mapped).toBeInstanceOf(Observable); expect(mapped).not.toBeInstanceOf(ObservableQuery); - await new Promise((resolve, reject) => { - const sub = mapped.subscribe({ - next(result) { - sub.unsubscribe(); - try { - expect(result).toEqual({ - loading: false, - networkStatus: NetworkStatus.ready, - data: { mapped: true }, - }); - } catch (error) { - reject(error); - return; - } - resolve(); - }, - error: reject, - }); + const stream = new ObservableStream(mapped); + + await expect(stream).toEmitApolloQueryResult({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { mapped: true }, }); + + await expect(stream).not.toEmitAnything(); }); }); diff --git a/src/testing/core/mocking/mockQueryManager.ts b/src/testing/core/mocking/mockQueryManager.ts deleted file mode 100644 index 5395dcc9c11..00000000000 --- a/src/testing/core/mocking/mockQueryManager.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { QueryManagerOptions } from "../../../core/QueryManager.js"; -import { QueryManager } from "../../../core/QueryManager.js"; -import type { MockedResponse } from "./mockLink.js"; -import { mockSingleLink } from "./mockLink.js"; -import { InMemoryCache } from "../../../cache/index.js"; -import { LocalState } from "../../../core/LocalState.js"; - -export const getDefaultOptionsForQueryManagerTests = ( - options: Pick, "cache" | "link"> & - Partial> -) => ({ - defaultOptions: Object.create(null), - documentTransform: undefined, - queryDeduplication: false, - onBroadcast: undefined, - ssrMode: false, - clientAwareness: {}, - localState: new LocalState({ cache: options.cache }), - assumeImmutableResults: !!options.cache.assumeImmutableResults, - defaultContext: undefined, - dataMasking: false, - ...options, -}); - -// Helper method for the tests that construct a query manager out of a -// a list of mocked responses for a mocked network interface. -export default (...mockedResponses: MockedResponse[]) => { - return new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link: mockSingleLink(...mockedResponses), - cache: new InMemoryCache({ addTypename: false }), - }) - ); -}; diff --git a/src/testing/core/mocking/mockWatchQuery.ts b/src/testing/core/mocking/mockWatchQuery.ts deleted file mode 100644 index b031de504c7..00000000000 --- a/src/testing/core/mocking/mockWatchQuery.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { MockedResponse } from "./mockLink.js"; -import mockQueryManager from "./mockQueryManager.js"; -import type { ObservableQuery } from "../../../core/index.js"; - -export default (...mockedResponses: MockedResponse[]): ObservableQuery => { - const queryManager = mockQueryManager(...mockedResponses); - const firstRequest = mockedResponses[0].request; - return queryManager.watchQuery({ - query: firstRequest.query!, - variables: firstRequest.variables, - notifyOnNetworkStatusChange: false, // XXX might not always be the right option. Set for legacy reasons. - }); -}; diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index f14b0098f6a..9c1a25d791c 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -57,6 +57,12 @@ interface ApolloCustomMatchers { (error?: any, options?: TakeOptions) => Promise : { error: "matcher needs to be called on an ObservableStream instance" }; + toEmitFetchResult: T extends ObservableStream> ? + (value: FetchResult, options?: TakeOptions) => Promise + : { + error: "matcher needs to be called on an ObservableStream> instance"; + }; + /** * Used to determine if the observable stream emitted a `next` event. Use * `toEmitValue` to check if the `next` event emitted a specific value. diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts index f0453f74312..0f71f6be96e 100644 --- a/src/testing/matchers/index.ts +++ b/src/testing/matchers/index.ts @@ -7,6 +7,7 @@ import { toComplete } from "./toComplete.js"; import { toEmitApolloQueryResult } from "./toEmitApolloQueryResult.js"; import { toEmitAnything } from "./toEmitAnything.js"; import { toEmitError } from "./toEmitError.js"; +import { toEmitFetchResult } from "./toEmitFetchResult.js"; import { toEmitMatchedValue } from "./toEmitMatchedValue.js"; import { toEmitNext } from "./toEmitNext.js"; import { toEmitValue } from "./toEmitValue.js"; @@ -20,6 +21,7 @@ expect.extend({ toEmitApolloQueryResult, toEmitAnything, toEmitError, + toEmitFetchResult, toEmitMatchedValue, toEmitNext, toEmitValue, diff --git a/src/testing/matchers/toEmitFetchResult.ts b/src/testing/matchers/toEmitFetchResult.ts new file mode 100644 index 00000000000..6d83aad10a6 --- /dev/null +++ b/src/testing/matchers/toEmitFetchResult.ts @@ -0,0 +1,64 @@ +import { iterableEquality } from "@jest/expect-utils"; +import type { MatcherFunction } from "expect"; +import type { ObservableStream } from "../internal/index.js"; +import type { TakeOptions } from "../internal/ObservableStream.js"; +import type { FetchResult } from "../../core/index.js"; + +export const toEmitFetchResult: MatcherFunction< + [queryResult: FetchResult, options?: TakeOptions] +> = async function (actual, expected, options) { + const stream = actual as ObservableStream; + const hint = this.utils.matcherHint( + this.isNot ? ".not.toEmitFetchResult" : "toEmitFetchResult", + "stream", + "expected" + ); + + try { + const value = await stream.takeNext(options); + const pass = this.equals( + value, + expected, + // https://github.com/jestjs/jest/blob/22029ba06b69716699254bb9397f2b3bc7b3cf3b/packages/expect/src/matchers.ts#L62-L67 + [...this.customTesters, iterableEquality], + true + ); + + return { + pass, + message: () => { + if (pass) { + return ( + hint + + "\n\nExpected stream not to emit a fetch result equal to expected but it did." + ); + } + + return ( + hint + + "\n\n" + + this.utils.printDiffOrStringify( + expected, + value, + "Expected", + "Received", + true + ) + ); + }, + }; + } catch (error) { + if ( + error instanceof Error && + error.message === "Timeout waiting for next event" + ) { + return { + pass: false, + message: () => + hint + "\n\nExpected stream to emit a value but it did not.", + }; + } else { + throw error; + } + } +};