From cda321ccfe00286a4912991b7c9cfc524c10672d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 10 Jan 2025 17:46:30 -0700 Subject: [PATCH] Remove `itAsync` from remaining relevant tests (#12210) --- src/__tests__/graphqlSubscriptions.ts | 210 +-- src/__tests__/optimistic.ts | 26 +- src/__tests__/refetchQueries.ts | 1582 ++++++++--------- src/link/schema/__tests__/schemaLink.ts | 184 +- src/link/utils/__tests__/toPromise.ts | 12 +- src/testing/internal/ObservableStream.ts | 51 +- .../observables/__tests__/Concast.ts | 178 +- 7 files changed, 1050 insertions(+), 1193 deletions(-) diff --git a/src/__tests__/graphqlSubscriptions.ts b/src/__tests__/graphqlSubscriptions.ts index d666ed22e65..2008c92285f 100644 --- a/src/__tests__/graphqlSubscriptions.ts +++ b/src/__tests__/graphqlSubscriptions.ts @@ -4,9 +4,9 @@ import { ApolloClient, FetchResult } from "../core"; import { InMemoryCache } from "../cache"; import { ApolloError, PROTOCOL_ERRORS_SYMBOL } from "../errors"; import { QueryManager } from "../core/QueryManager"; -import { itAsync, mockObservableLink } from "../testing"; +import { mockObservableLink } from "../testing"; import { GraphQLError } from "graphql"; -import { spyOnConsole } from "../testing/internal"; +import { ObservableStream, spyOnConsole } from "../testing/internal"; import { getDefaultOptionsForQueryManagerTests } from "../testing/core/mocking/mockQueryManager"; describe("GraphQL Subscriptions", () => { @@ -47,36 +47,23 @@ describe("GraphQL Subscriptions", () => { }; }); - itAsync( - "should start a subscription on network interface and unsubscribe", - (resolve, reject) => { - const link = mockObservableLink(); - // This test calls directly through Apollo Client - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + it("should start a subscription on network interface and unsubscribe", async () => { + const link = mockObservableLink(); + // This test calls directly through Apollo Client + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - let count = 0; - const sub = client.subscribe(defaultOptions).subscribe({ - next(result) { - count++; - expect(result).toEqual(results[0].result); + const stream = new ObservableStream(client.subscribe(defaultOptions)); + link.simulateResult(results[0]); - // Test unsubscribing - if (count > 1) { - throw new Error("next fired after unsubscribing"); - } - sub.unsubscribe(); - resolve(); - }, - }); + await expect(stream).toEmitValue(results[0].result); - link.simulateResult(results[0]); - } - ); + stream.unsubscribe(); + }); - itAsync("should subscribe with default values", (resolve, reject) => { + it("should subscribe with default values", async () => { const link = mockObservableLink(); // This test calls directly through Apollo Client const client = new ApolloClient({ @@ -84,25 +71,16 @@ describe("GraphQL Subscriptions", () => { cache: new InMemoryCache({ addTypename: false }), }); - let count = 0; - const sub = client.subscribe(options).subscribe({ - next(result) { - expect(result).toEqual(results[0].result); + const stream = new ObservableStream(client.subscribe(options)); - // Test unsubscribing - if (count > 1) { - throw new Error("next fired after unsubscribing"); - } - sub.unsubscribe(); + link.simulateResult(results[0]); - resolve(); - }, - }); + await expect(stream).toEmitValue(results[0].result); - link.simulateResult(results[0]); + stream.unsubscribe(); }); - itAsync("should multiplex subscriptions", (resolve, reject) => { + it("should multiplex subscriptions", async () => { const link = mockObservableLink(); const queryManager = new QueryManager( getDefaultOptionsForQueryManagerTests({ @@ -112,88 +90,57 @@ describe("GraphQL Subscriptions", () => { ); const obs = queryManager.startGraphQLSubscription(options); - - let counter = 0; - - // tslint:disable-next-line - obs.subscribe({ - next(result) { - expect(result).toEqual(results[0].result); - counter++; - if (counter === 2) { - resolve(); - } - }, - }) as any; - - // Subscribe again. Should also receive the same result. - // tslint:disable-next-line - obs.subscribe({ - next(result) { - expect(result).toEqual(results[0].result); - counter++; - if (counter === 2) { - resolve(); - } - }, - }) as any; + 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); }); - itAsync( - "should receive multiple results for a subscription", - (resolve, reject) => { - const link = mockObservableLink(); - let numResults = 0; - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache: new InMemoryCache({ addTypename: false }), - }) - ); - - // tslint:disable-next-line - queryManager.startGraphQLSubscription(options).subscribe({ - next(result) { - expect(result).toEqual(results[numResults].result); - numResults++; - if (numResults === 4) { - resolve(); - } - }, - }) as any; + it("should receive multiple results for a subscription", async () => { + const link = mockObservableLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache({ addTypename: false }), + }) + ); - for (let i = 0; i < 4; i++) { - link.simulateResult(results[i]); - } + const stream = new ObservableStream( + queryManager.startGraphQLSubscription(options) + ); + + for (let i = 0; i < 4; i++) { + link.simulateResult(results[i]); } - ); - - itAsync( - "should not cache subscription data if a `no-cache` fetch policy is used", - (resolve, reject) => { - const link = mockObservableLink(); - const cache = new InMemoryCache({ addTypename: false }); - const client = new ApolloClient({ - link, - cache, - }); - expect(cache.extract()).toEqual({}); + 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).not.toEmitAnything(); + }); - options.fetchPolicy = "no-cache"; - const sub = client.subscribe(options).subscribe({ - next() { - expect(cache.extract()).toEqual({}); - sub.unsubscribe(); - resolve(); - }, - }); + it("should not cache subscription data if a `no-cache` fetch policy is used", async () => { + const link = mockObservableLink(); + const cache = new InMemoryCache({ addTypename: false }); + const client = new ApolloClient({ + link, + cache, + }); - link.simulateResult(results[0]); - } - ); + expect(cache.extract()).toEqual({}); + + options.fetchPolicy = "no-cache"; + const stream = new ObservableStream(client.subscribe(options)); + + link.simulateResult(results[0]); + + await expect(stream).toEmitNext(); + expect(cache.extract()).toEqual({}); + }); it("should throw an error if the result has errors on it", () => { const link = mockObservableLink(); @@ -492,27 +439,22 @@ describe("GraphQL Subscriptions", () => { }); }); - itAsync( - "should pass a context object through the link execution chain", - (resolve, reject) => { - const link = mockObservableLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - }); + it("should pass a context object through the link execution chain", async () => { + const link = mockObservableLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + }); - client.subscribe(options).subscribe({ - next() { - expect(link.operation?.getContext().someVar).toEqual( - options.context.someVar - ); - resolve(); - }, - }); + const stream = new ObservableStream(client.subscribe(options)); - link.simulateResult(results[0]); - } - ); + link.simulateResult(results[0]); + + await expect(stream).toEmitNext(); + expect(link.operation?.getContext().someVar).toEqual( + options.context.someVar + ); + }); it("should throw an error if the result has protocolErrors on it", async () => { const link = mockObservableLink(); diff --git a/src/__tests__/optimistic.ts b/src/__tests__/optimistic.ts index d8d41511a6c..f2b20a94e84 100644 --- a/src/__tests__/optimistic.ts +++ b/src/__tests__/optimistic.ts @@ -240,7 +240,7 @@ describe("optimistic mutation results", () => { }); it("handles errors produced by one mutation in a series", async () => { - expect.assertions(12); + expect.assertions(11); const client = await setup( { request: { query: mutation }, @@ -303,7 +303,7 @@ describe("optimistic mutation results", () => { }); it("can run 2 mutations concurrently and handles all intermediate states well", async () => { - expect.assertions(36); + expect.assertions(35); function checkBothMutationsAreApplied( expectedText1: any, expectedText2: any @@ -466,7 +466,7 @@ describe("optimistic mutation results", () => { }); it("handles errors produced by one mutation in a series", async () => { - expect.assertions(12); + expect.assertions(11); const client = await setup( { request: { query: mutation }, @@ -528,7 +528,7 @@ describe("optimistic mutation results", () => { }); it("can run 2 mutations concurrently and handles all intermediate states well", async () => { - expect.assertions(36); + expect.assertions(35); function checkBothMutationsAreApplied( expectedText1: any, expectedText2: any @@ -831,7 +831,7 @@ describe("optimistic mutation results", () => { }); it("will use a passed variable in optimisticResponse", async () => { - expect.assertions(8); + expect.assertions(7); const client = await setup({ request: { query: mutation, variables }, result: mutationResult, @@ -893,7 +893,7 @@ describe("optimistic mutation results", () => { }); it("will not update optimistically if optimisticResponse returns IGNORE sentinel object", async () => { - expect.assertions(7); + expect.assertions(6); const client = await setup({ request: { query: mutation, variables }, @@ -1068,7 +1068,7 @@ describe("optimistic mutation results", () => { }; it("will insert a single itemAsync to the beginning", async () => { - expect.assertions(9); + expect.assertions(8); const client = await setup({ request: { query: mutation }, result: mutationResult, @@ -1116,7 +1116,7 @@ describe("optimistic mutation results", () => { }); it("two array insert like mutations", async () => { - expect.assertions(11); + expect.assertions(10); const client = await setup( { request: { query: mutation }, @@ -1198,7 +1198,7 @@ describe("optimistic mutation results", () => { }); it("two mutations, one fails", async () => { - expect.assertions(12); + expect.assertions(11); const client = await setup( { request: { query: mutation }, @@ -1452,7 +1452,7 @@ describe("optimistic mutation results", () => { }; it("will insert a single itemAsync to the beginning", async () => { - expect.assertions(8); + expect.assertions(7); const client = await setup({ request: { query: mutation }, delay: 300, @@ -1510,7 +1510,7 @@ describe("optimistic mutation results", () => { }); it("two array insert like mutations", async () => { - expect.assertions(11); + expect.assertions(10); const client = await setup( { request: { query: mutation }, @@ -1610,7 +1610,7 @@ describe("optimistic mutation results", () => { }); it("two mutations, one fails", async () => { - expect.assertions(12); + expect.assertions(11); const client = await setup( { request: { query: mutation }, @@ -2299,7 +2299,7 @@ describe("optimistic mutation - githunt comments", () => { }; it("can post a new comment", async () => { - expect.assertions(3); + expect.assertions(2); const mutationVariables = { repoFullName: "org/repo", commentContent: "New Comment", diff --git a/src/__tests__/refetchQueries.ts b/src/__tests__/refetchQueries.ts index 4b843c61165..fbeb52c6012 100644 --- a/src/__tests__/refetchQueries.ts +++ b/src/__tests__/refetchQueries.ts @@ -1,6 +1,5 @@ import { Subscription } from "zen-observable-ts"; -import { itAsync } from "../testing"; import { ApolloClient, ApolloLink, @@ -10,29 +9,31 @@ import { TypedDocumentNode, ObservableQuery, } from "../core"; +import { ObservableStream } from "../testing/internal"; describe("client.refetchQueries", () => { - itAsync("is public and callable", (resolve, reject) => { + it("is public and callable", async () => { + expect.assertions(6); const client = new ApolloClient({ cache: new InMemoryCache(), }); expect(typeof client.refetchQueries).toBe("function"); + const onQueryUpdated = jest.fn(); const result = client.refetchQueries({ updateCache(cache) { expect(cache).toBe(client.cache); expect(cache.extract()).toEqual({}); }, - onQueryUpdated() { - reject("should not have called onQueryUpdated"); - return false; - }, + onQueryUpdated, }); expect(result.queries).toEqual([]); expect(result.results).toEqual([]); - result.then(resolve, reject); + await result; + + expect(onQueryUpdated).not.toHaveBeenCalled(); }); const aQuery: TypedDocumentNode<{ a: string }> = gql` @@ -113,917 +114,858 @@ describe("client.refetchQueries", () => { }); } - itAsync( - "includes watched queries affected by updateCache", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + it("includes watched queries affected by updateCache", async () => { + expect.assertions(9); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - const ayyResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + const ayyResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - reject("bQuery should not have been updated"); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + throw new Error("bQuery should not have been updated"); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - sortObjects(ayyResults); + sortObjects(ayyResults); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - // Note that no bQuery result is included here. - ]); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + // Note that no bQuery result is included here. + ]); - const beeResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + const beeResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - onQueryUpdated(obs, diff) { - if (obs === aObs) { - reject("aQuery should not have been updated"); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + onQueryUpdated(obs, diff) { + if (obs === aObs) { + throw new Error("aQuery should not have been updated"); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - sortObjects(beeResults); + sortObjects(beeResults); - expect(beeResults).toEqual([ - // Note that no aQuery result is included here. - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + expect(beeResults).toEqual([ + // Note that no aQuery result is included here. + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - unsubscribe(); - resolve(); - } - ); + unsubscribe(); + }); - itAsync( - "includes watched queries named in options.include", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + it("includes watched queries named in options.include", async () => { + expect.assertions(11); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - const ayyResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + const ayyResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - // This is the options.include array mentioned in the test description. - include: ["B"], + // This is the options.include array mentioned in the test description. + include: ["B"], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(ayyResults); - sortObjects(ayyResults); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + // Included this time! + { b: "B" }, + ]); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - // Included this time! - { b: "B" }, - ]); + const beeResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - const beeResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + // The "A" here causes aObs to be included, but the "AB" should be + // redundant because that query is already included. + include: ["A", "AB"], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - // The "A" here causes aObs to be included, but the "AB" should be - // redundant because that query is already included. - include: ["A", "AB"], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + sortObjects(beeResults); - sortObjects(beeResults); + expect(beeResults).toEqual([ + { a: "Ayy" }, // Included this time! + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - expect(beeResults).toEqual([ - { a: "Ayy" }, // Included this time! - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + unsubscribe(); + }); - unsubscribe(); - resolve(); - } - ); + it("includes query DocumentNode objects specified in options.include", async () => { + expect.assertions(11); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - itAsync( - "includes query DocumentNode objects specified in options.include", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + const ayyResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - const ayyResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + // Note that we're passing query DocumentNode objects instead of query + // name strings, in this test. + include: [bQuery, abQuery], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - // Note that we're passing query DocumentNode objects instead of query - // name strings, in this test. - include: [bQuery, abQuery], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(ayyResults); - sortObjects(ayyResults); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + // Included this time! + { b: "B" }, + ]); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - // Included this time! - { b: "B" }, - ]); + const beeResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - const beeResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + // The abQuery and "AB" should be redundant, but the aQuery here is + // important for aObs to be included. + include: [abQuery, "AB", aQuery], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - // The abQuery and "AB" should be redundant, but the aQuery here is - // important for aObs to be included. - include: [abQuery, "AB", aQuery], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + sortObjects(beeResults); - sortObjects(beeResults); + expect(beeResults).toEqual([ + { a: "Ayy" }, // Included this time! + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - expect(beeResults).toEqual([ - { a: "Ayy" }, // Included this time! - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + unsubscribe(); + }); - unsubscribe(); - resolve(); - } - ); + it('includes all queries when options.include === "all"', async () => { + expect.assertions(11); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - itAsync( - 'includes all queries when options.include === "all"', - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + const ayyResults = await client.refetchQueries({ + include: "all", - const ayyResults = await client.refetchQueries({ - include: "all", + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(ayyResults); - sortObjects(ayyResults); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + { b: "B" }, + ]); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - { b: "B" }, - ]); + const beeResults = await client.refetchQueries({ + include: "all", - const beeResults = await client.refetchQueries({ - include: "all", + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + sortObjects(beeResults); - sortObjects(beeResults); + expect(beeResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - expect(beeResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + unsubscribe(); + }); - unsubscribe(); - resolve(); - } - ); - - itAsync( - 'includes all active queries when options.include === "active"', - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const extraObs = client.watchQuery({ query: abQuery }); - expect(extraObs.hasObservers()).toBe(false); - - const activeResults = await client.refetchQueries({ - include: "active", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + it('includes all active queries when options.include === "active"', async () => { + expect.assertions(15); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const extraObs = client.watchQuery({ query: abQuery }); + expect(extraObs.hasObservers()).toBe(false); + + const activeResults = await client.refetchQueries({ + include: "active", + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - sortObjects(activeResults); + sortObjects(activeResults); - expect(activeResults).toEqual([ - { a: "A" }, - { a: "A", b: "B" }, - { b: "B" }, - ]); + expect(activeResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - subs.push( - extraObs.subscribe({ - next(result) { - expect(result).toEqual({ a: "A", b: "B" }); - }, - }) - ); - expect(extraObs.hasObservers()).toBe(true); - - const resultsAfterSubscribe = await client.refetchQueries({ - include: "active", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else if (obs === extraObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); + subs.push( + extraObs.subscribe({ + next(result) { + expect(result).toEqual({ a: "A", b: "B" }); }, - }); - - sortObjects(resultsAfterSubscribe); + }) + ); + expect(extraObs.hasObservers()).toBe(true); + + const resultsAfterSubscribe = await client.refetchQueries({ + include: "active", + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else if (obs === extraObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - expect(resultsAfterSubscribe).toEqual([ - { a: "A" }, - { a: "A", b: "B" }, - // Included thanks to extraObs this time. - { a: "A", b: "B" }, - // Sorted last by sortObjects. - { b: "B" }, - ]); + sortObjects(resultsAfterSubscribe); - unsubscribe(); - resolve(); - } - ); - - itAsync( - "includes queries named in refetchQueries even if they have no observers", - async (resolve, reject) => { - const client = makeClient(); - - const aObs = client.watchQuery({ query: aQuery }); - const bObs = client.watchQuery({ query: bQuery }); - const abObs = client.watchQuery({ query: abQuery }); - - // These ObservableQuery objects have no observers yet, but should - // nevertheless be refetched if identified explicitly in an options.include - // array passed to client.refetchQueries. - expect(aObs.hasObservers()).toBe(false); - expect(bObs.hasObservers()).toBe(false); - expect(abObs.hasObservers()).toBe(false); - - const activeResults = await client.refetchQueries({ - include: ["A", abQuery], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.complete).toBe(false); - expect(diff.result).toEqual({}); - } else if (obs === abObs) { - expect(diff.complete).toBe(false); - expect(diff.result).toEqual({}); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + expect(resultsAfterSubscribe).toEqual([ + { a: "A" }, + { a: "A", b: "B" }, + // Included thanks to extraObs this time. + { a: "A", b: "B" }, + // Sorted last by sortObjects. + { b: "B" }, + ]); - sortObjects(activeResults); - expect(activeResults).toEqual([{}, {}]); - - subs.push( - abObs.subscribe({ - next(result) { - expect(result.data).toEqual({ a: "A", b: "B" }); - - client - .refetchQueries({ - include: [aQuery, "B"], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }) - .then((resultsAfterSubscribe) => { - sortObjects(resultsAfterSubscribe); - expect(resultsAfterSubscribe).toEqual([{ a: "A" }, { b: "B" }]); - - unsubscribe(); - }) - .then(resolve, reject); - }, - }) - ); + unsubscribe(); + }); - expect(abObs.hasObservers()).toBe(true); - } - ); - - itAsync( - "should not include unwatched single queries", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const delayedQuery = gql` - query DELAYED { - d - e - l - a - y - e - d + it("includes queries named in refetchQueries even if they have no observers", async () => { + expect.assertions(13); + const client = makeClient(); + + const aObs = client.watchQuery({ query: aQuery }); + const bObs = client.watchQuery({ query: bQuery }); + const abObs = client.watchQuery({ query: abQuery }); + + // These ObservableQuery objects have no observers yet, but should + // nevertheless be refetched if identified explicitly in an options.include + // array passed to client.refetchQueries. + expect(aObs.hasObservers()).toBe(false); + expect(bObs.hasObservers()).toBe(false); + expect(abObs.hasObservers()).toBe(false); + + const activeResults = await client.refetchQueries({ + include: ["A", abQuery], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.complete).toBe(false); + expect(diff.result).toEqual({}); + } else if (obs === abObs) { + expect(diff.complete).toBe(false); + expect(diff.result).toEqual({}); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); } - `; - - client - .query({ - query: delayedQuery, - variables: { - // Delay this query by 10 seconds so it stays in-flight. - delay: 10000, - }, - }) - .catch(reject); - - const queries = client["queryManager"]["queries"]; - expect(queries.size).toBe(4); - - queries.forEach((queryInfo, queryId) => { - if (queryId === "1" || queryId === "2" || queryId === "3") { - expect(queryInfo.observableQuery).toBeInstanceOf(ObservableQuery); - } else if (queryId === "4") { - // One-off client.query-style queries never get an ObservableQuery, so - // they should not be included by include: "active". - expect(queryInfo.observableQuery).toBe(null); - expect(queryInfo.document).toBe(delayedQuery); + return Promise.resolve(diff.result); + }, + }); + + sortObjects(activeResults); + expect(activeResults).toEqual([{}, {}]); + + const stream = new ObservableStream(abObs); + subs.push(stream as unknown as Subscription); + expect(abObs.hasObservers()).toBe(true); + + await expect(stream).toEmitMatchedValue({ data: { a: "A", b: "B" } }); + + const resultsAfterSubscribe = await client.refetchQueries({ + include: [aQuery, "B"], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); } - }); + return Promise.resolve(diff.result); + }, + }); - const activeResults = await client.refetchQueries({ - include: "active", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(resultsAfterSubscribe); + expect(resultsAfterSubscribe).toEqual([{ a: "A" }, { b: "B" }]); - sortObjects(activeResults); + unsubscribe(); + }); - expect(activeResults).toEqual([ - { a: "A" }, - { a: "A", b: "B" }, - { b: "B" }, - ]); + it("should not include unwatched single queries", async () => { + expect.assertions(18); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const delayedQuery = gql` + query DELAYED { + d + e + l + a + y + e + d + } + `; + + void client.query({ + query: delayedQuery, + variables: { + // Delay this query by 10 seconds so it stays in-flight. + delay: 10000, + }, + }); - const allResults = await client.refetchQueries({ - include: "all", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + const queries = client["queryManager"]["queries"]; + expect(queries.size).toBe(4); + + queries.forEach((queryInfo, queryId) => { + if (queryId === "1" || queryId === "2" || queryId === "3") { + expect(queryInfo.observableQuery).toBeInstanceOf(ObservableQuery); + } else if (queryId === "4") { + // One-off client.query-style queries never get an ObservableQuery, so + // they should not be included by include: "active". + expect(queryInfo.observableQuery).toBe(null); + expect(queryInfo.document).toBe(delayedQuery); + } + }); - sortObjects(allResults); + const activeResults = await client.refetchQueries({ + include: "active", + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - expect(allResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); + sortObjects(activeResults); - unsubscribe(); - client.stop(); + expect(activeResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - expect(queries.size).toBe(0); + const allResults = await client.refetchQueries({ + include: "all", - resolve(); - } - ); - - itAsync( - "refetches watched queries if onQueryUpdated not provided", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const aSpy = jest.spyOn(aObs, "refetch"); - const bSpy = jest.spyOn(bObs, "refetch"); - const abSpy = jest.spyOn(abObs, "refetch"); - - const ayyResults = ( - await client.refetchQueries({ - include: ["B"], - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, - }) - ).map((result) => result.data as object); + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - sortObjects(ayyResults); + sortObjects(allResults); - // These results have reverted back to what the ApolloLink returns ("A" - // rather than "Ayy"), because we let them be refetched (by not providing - // an onQueryUpdated function). - expect(ayyResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); + expect(allResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - expect(aSpy).toHaveBeenCalledTimes(1); - expect(bSpy).toHaveBeenCalledTimes(1); - expect(abSpy).toHaveBeenCalledTimes(1); + unsubscribe(); + client.stop(); - unsubscribe(); - resolve(); - } - ); - - itAsync( - "can run updateQuery function against optimistic cache layer", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - client.cache.watch({ - query: abQuery, - optimistic: false, - callback(diff) { - reject("should not have notified non-optimistic watcher"); - }, - }); + expect(queries.size).toBe(0); + }); - expect(client.cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "B", - }, - }); + it("refetches watched queries if onQueryUpdated not provided", async () => { + expect.assertions(7); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - const results = await client.refetchQueries({ - // This causes the update to run against a temporary optimistic layer. - optimistic: true, + const aSpy = jest.spyOn(aObs, "refetch"); + const bSpy = jest.spyOn(bObs, "refetch"); + const abSpy = jest.spyOn(abObs, "refetch"); + const ayyResults = ( + await client.refetchQueries({ + include: ["B"], updateCache(cache) { - const modified = cache.modify({ - fields: { - a(value, { DELETE }) { - expect(value).toEqual("A"); - return DELETE; - }, + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", }, }); - expect(modified).toBe(true); }, + }) + ).map((result) => result.data as object); - onQueryUpdated(obs, diff) { - expect(diff.complete).toBe(true); - - // Even though we evicted the Query.a field in the updateCache function, - // that optimistic layer was discarded before broadcasting results, so - // we're back to the original (non-optimistic) data. - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - reject("bQuery should not have been updated"); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - - return diff.result; - }, - }); + sortObjects(ayyResults); - sortObjects(results); + // These results have reverted back to what the ApolloLink returns ("A" + // rather than "Ayy"), because we let them be refetched (by not providing + // an onQueryUpdated function). + expect(ayyResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - expect(results).toEqual([{ a: "A" }, { a: "A", b: "B" }]); + expect(aSpy).toHaveBeenCalledTimes(1); + expect(bSpy).toHaveBeenCalledTimes(1); + expect(abSpy).toHaveBeenCalledTimes(1); - expect(client.cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "B", - }, - }); + unsubscribe(); + }); - resolve(); - } - ); - - itAsync( - "can return true from onQueryUpdated to choose default refetching behavior", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const refetchResult = client.refetchQueries({ - include: ["A", "B"], - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - reject("abQuery should not have been updated"); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return true; - }, - }); + it("can run updateQuery function against optimistic cache layer", async () => { + expect.assertions(12); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - expect(refetchResult.results.length).toBe(2); - refetchResult.results.forEach((result) => { - expect(result).toBeInstanceOf(Promise); - }); + client.cache.watch({ + query: abQuery, + optimistic: false, + callback(diff) { + throw new Error("should not have notified non-optimistic watcher"); + }, + }); - expect( - refetchResult.queries - .map((obs) => { - expect(obs).toBeInstanceOf(ObservableQuery); - return obs.queryName; - }) - .sort() - ).toEqual(["A", "B"]); - - const results = (await refetchResult).map((result) => { - // These results are ApolloQueryResult, as inferred by TypeScript. - expect(Object.keys(result).sort()).toEqual([ - "data", - "loading", - "networkStatus", - ]); - return result.data; - }); + expect(client.cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "B", + }, + }); - sortObjects(results); + const results = await client.refetchQueries({ + // This causes the update to run against a temporary optimistic layer. + optimistic: true, - expect(results).toEqual([{ a: "A" }, { b: "B" }]); + updateCache(cache) { + const modified = cache.modify({ + fields: { + a(value, { DELETE }) { + expect(value).toEqual("A"); + return DELETE; + }, + }, + }); + expect(modified).toBe(true); + }, - resolve(); - } - ); + onQueryUpdated(obs, diff) { + expect(diff.complete).toBe(true); + + // Even though we evicted the Query.a field in the updateCache function, + // that optimistic layer was discarded before broadcasting results, so + // we're back to the original (non-optimistic) data. + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + throw new Error("bQuery should not have been updated"); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } - itAsync( - "can return true from onQueryUpdated when using options.updateCache", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + return diff.result; + }, + }); - const refetchResult = client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Beetlejuice", - }, - }); - }, + sortObjects(results); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - reject("aQuery should not have been updated"); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Beetlejuice" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "Beetlejuice" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - - expect(client.cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "Beetlejuice", - }, - }); + expect(results).toEqual([{ a: "A" }, { a: "A", b: "B" }]); - return true; - }, - }); + expect(client.cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "B", + }, + }); + }); - expect(refetchResult.results.length).toBe(2); - refetchResult.results.forEach((result) => { - expect(result).toBeInstanceOf(Promise); - }); + it("can return true from onQueryUpdated to choose default refetching behavior", async () => { + expect.assertions(14); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const refetchResult = client.refetchQueries({ + include: ["A", "B"], + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + throw new Error("abQuery should not have been updated"); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return true; + }, + }); - expect( - refetchResult.queries - .map((obs) => { - expect(obs).toBeInstanceOf(ObservableQuery); - return obs.queryName; - }) - .sort() - ).toEqual(["AB", "B"]); - - const results = (await refetchResult).map((result) => { - // These results are ApolloQueryResult, as inferred by TypeScript. - expect(Object.keys(result).sort()).toEqual([ - "data", - "loading", - "networkStatus", - ]); - return result.data; - }); + expect(refetchResult.results.length).toBe(2); + refetchResult.results.forEach((result) => { + expect(result).toBeInstanceOf(Promise); + }); + + expect( + refetchResult.queries + .map((obs) => { + expect(obs).toBeInstanceOf(ObservableQuery); + return obs.queryName; + }) + .sort() + ).toEqual(["A", "B"]); + + const results = (await refetchResult).map((result) => { + // These results are ApolloQueryResult, as inferred by TypeScript. + expect(Object.keys(result).sort()).toEqual([ + "data", + "loading", + "networkStatus", + ]); + return result.data; + }); + + sortObjects(results); + + expect(results).toEqual([{ a: "A" }, { b: "B" }]); + }); - sortObjects(results); + it("can return true from onQueryUpdated when using options.updateCache", async () => { + expect.assertions(17); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - expect(results).toEqual([ - // Since we returned true from onQueryUpdated, the results were refetched, - // replacing "Beetlejuice" with "B" again. - { a: "A", b: "B" }, - { b: "B" }, + const refetchResult = client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Beetlejuice", + }, + }); + }, + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + throw new Error("aQuery should not have been updated"); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Beetlejuice" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "Beetlejuice" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + + expect(client.cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "Beetlejuice", + }, + }); + + return true; + }, + }); + + expect(refetchResult.results.length).toBe(2); + refetchResult.results.forEach((result) => { + expect(result).toBeInstanceOf(Promise); + }); + + expect( + refetchResult.queries + .map((obs) => { + expect(obs).toBeInstanceOf(ObservableQuery); + return obs.queryName; + }) + .sort() + ).toEqual(["AB", "B"]); + + const results = (await refetchResult).map((result) => { + // These results are ApolloQueryResult, as inferred by TypeScript. + expect(Object.keys(result).sort()).toEqual([ + "data", + "loading", + "networkStatus", ]); + return result.data; + }); - expect(client.cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "B", - }, - }); + sortObjects(results); - resolve(); - } - ); - - itAsync( - "can return false from onQueryUpdated to skip/ignore a query", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const refetchResult = client.refetchQueries({ - include: ["A", "B"], - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - reject("abQuery should not have been updated"); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - // Skip refetching all but the B query. - return obs.queryName === "B"; - }, - }); + expect(results).toEqual([ + // Since we returned true from onQueryUpdated, the results were refetched, + // replacing "Beetlejuice" with "B" again. + { a: "A", b: "B" }, + { b: "B" }, + ]); - expect(refetchResult.results.length).toBe(1); - refetchResult.results.forEach((result) => { - expect(result).toBeInstanceOf(Promise); - }); + expect(client.cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "B", + }, + }); + }); - expect( - refetchResult.queries - .map((obs) => { - expect(obs).toBeInstanceOf(ObservableQuery); - return obs.queryName; - }) - .sort() - ).toEqual(["B"]); - - const results = (await refetchResult).map((result) => { - // These results are ApolloQueryResult, as inferred by TypeScript. - expect(Object.keys(result).sort()).toEqual([ - "data", - "loading", - "networkStatus", - ]); - return result.data; - }); + it("can return false from onQueryUpdated to skip/ignore a query", async () => { + expect.assertions(11); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const refetchResult = client.refetchQueries({ + include: ["A", "B"], + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + throw new Error("abQuery should not have been updated"); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + // Skip refetching all but the B query. + return obs.queryName === "B"; + }, + }); - sortObjects(results); + expect(refetchResult.results.length).toBe(1); + refetchResult.results.forEach((result) => { + expect(result).toBeInstanceOf(Promise); + }); - expect(results).toEqual([{ b: "B" }]); + expect( + refetchResult.queries + .map((obs) => { + expect(obs).toBeInstanceOf(ObservableQuery); + return obs.queryName; + }) + .sort() + ).toEqual(["B"]); + + const results = (await refetchResult).map((result) => { + // These results are ApolloQueryResult, as inferred by TypeScript. + expect(Object.keys(result).sort()).toEqual([ + "data", + "loading", + "networkStatus", + ]); + return result.data; + }); - resolve(); - } - ); + sortObjects(results); + + expect(results).toEqual([{ b: "B" }]); + }); it("can refetch no-cache queries", () => { // TODO The options.updateCache function won't work for these queries, but diff --git a/src/link/schema/__tests__/schemaLink.ts b/src/link/schema/__tests__/schemaLink.ts index d4031b679ca..a536351002d 100644 --- a/src/link/schema/__tests__/schemaLink.ts +++ b/src/link/schema/__tests__/schemaLink.ts @@ -3,7 +3,7 @@ import gql from "graphql-tag"; import { execute } from "../../core/execute"; import { SchemaLink } from "../"; -import { itAsync } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const sampleQuery = gql` query SampleQuery { @@ -51,25 +51,18 @@ describe("SchemaLink", () => { expect(link.schema).toEqual(schema); }); - itAsync("calls next and then complete", (resolve, reject) => { - const next = jest.fn(); + it("calls next and then complete", async () => { const link = new SchemaLink({ schema }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe({ - next, - error: () => { - throw new Error("Received error"); - }, - complete: () => { - expect(next).toHaveBeenCalledTimes(1); - resolve(); - }, - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = new SchemaLink({ validate: true, schema: makeExecutableSchema({ @@ -86,98 +79,67 @@ describe("SchemaLink", () => { const observable = execute(link, { query: sampleQuery, }); - observable.subscribe((result) => { - expect(result.errors).toBeTruthy(); - expect(result.errors!.length).toBe(1); - expect(result.errors![0].message).toMatch(/Unauthorized/); - resolve(); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: { sampleQuery: null }, + errors: [{ message: "Unauthorized", path: ["sampleQuery"] }], }); }); - itAsync( - "supports query which is executed synchronously", - (resolve, reject) => { - const next = jest.fn(); - const link = new SchemaLink({ schema }); - const introspectionQuery = gql` - query IntrospectionQuery { - __schema { - types { - name - } + it("supports query which is executed synchronously", async () => { + const link = new SchemaLink({ schema }); + const introspectionQuery = gql` + query IntrospectionQuery { + __schema { + types { + name } } - `; - const observable = execute(link, { - query: introspectionQuery, - }); - observable.subscribe( - next, - () => { - throw new Error("Received error"); - }, - () => { - expect(next).toHaveBeenCalledTimes(1); - resolve(); - } - ); - } - ); - - itAsync( - "passes operation context into execute with context function", - (resolve, reject) => { - const next = jest.fn(); - const contextValue = { some: "value" }; - const contextProvider = jest.fn((operation) => operation.getContext()); - const resolvers = { - Query: { - sampleQuery: (root: any, args: any, context: any) => { - try { - expect(context).toEqual(contextValue); - } catch (error) { - reject("Should pass context into resolver"); - } - }, + } + `; + const observable = execute(link, { + query: introspectionQuery, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + }); + + it("passes operation context into execute with context function", async () => { + const contextValue = { some: "value" }; + const contextProvider = jest.fn((operation) => operation.getContext()); + const resolvers = { + Query: { + sampleQuery: (root: any, args: any, context: any) => { + expect(context).toEqual(contextValue); }, - }; - const schemaWithResolvers = makeExecutableSchema({ - typeDefs, - resolvers, - }); - const link = new SchemaLink({ - schema: schemaWithResolvers, - context: contextProvider, - }); - const observable = execute(link, { - query: sampleQuery, - context: contextValue, - }); - observable.subscribe( - next, - (error) => reject("Shouldn't call onError"), - () => { - try { - expect(next).toHaveBeenCalledTimes(1); - expect(contextProvider).toHaveBeenCalledTimes(1); - resolve(); - } catch (e) { - reject(e); - } - } - ); - } - ); + }, + }; + const schemaWithResolvers = makeExecutableSchema({ + typeDefs, + resolvers, + }); + const link = new SchemaLink({ + schema: schemaWithResolvers, + context: contextProvider, + }); + const observable = execute(link, { + query: sampleQuery, + context: contextValue, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + expect(contextProvider).toHaveBeenCalledTimes(1); + }); - itAsync("passes static context into execute", (resolve, reject) => { - const next = jest.fn(); + it("passes static context into execute", async () => { const contextValue = { some: "value" }; const resolver = jest.fn((root, args, context) => { - try { - expect(context).toEqual(contextValue); - } catch (error) { - reject("Should pass context into resolver"); - } + expect(context).toEqual(contextValue); }); const resolvers = { @@ -196,22 +158,14 @@ describe("SchemaLink", () => { const observable = execute(link, { query: sampleQuery, }); - observable.subscribe( - next, - (error) => reject("Shouldn't call onError"), - () => { - try { - expect(next).toHaveBeenCalledTimes(1); - expect(resolver).toHaveBeenCalledTimes(1); - resolve(); - } catch (e) { - reject(e); - } - } - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + expect(resolver).toHaveBeenCalledTimes(1); }); - itAsync("reports errors for unknown queries", (resolve, reject) => { + it("reports errors for unknown queries", async () => { const link = new SchemaLink({ validate: true, schema: makeExecutableSchema({ @@ -225,11 +179,9 @@ describe("SchemaLink", () => { } `, }); - observable.subscribe((result) => { - expect(result.errors).toBeTruthy(); - expect(result.errors!.length).toBe(1); - expect(result.errors![0].message).toMatch(/Cannot query field "unknown"/); - resolve(); + const stream = new ObservableStream(observable); + await expect(stream).toEmitValue({ + errors: [{ message: 'Cannot query field "unknown" on type "Query".' }], }); }); }); diff --git a/src/link/utils/__tests__/toPromise.ts b/src/link/utils/__tests__/toPromise.ts index c81f3eb27b2..82bf7c04083 100644 --- a/src/link/utils/__tests__/toPromise.ts +++ b/src/link/utils/__tests__/toPromise.ts @@ -1,5 +1,4 @@ import { Observable } from "../../../utilities/observables/Observable"; -import { itAsync } from "../../../testing"; import { toPromise } from "../toPromise"; import { fromError } from "../fromError"; @@ -38,12 +37,11 @@ describe("toPromise", () => { console.warn = _warn; }); - itAsync("return error call as Promise rejection", (resolve, reject) => { - toPromise(Observable.of(data, data)).then((result) => { - expect(data).toEqual(result); - expect(spy).toHaveBeenCalled(); - resolve(); - }); + it("return error call as Promise rejection", async () => { + const result = await toPromise(Observable.of(data, data)); + + expect(data).toEqual(result); + expect(spy).toHaveBeenCalled(); }); }); }); diff --git a/src/testing/internal/ObservableStream.ts b/src/testing/internal/ObservableStream.ts index f6c53169b87..ad5d7c05175 100644 --- a/src/testing/internal/ObservableStream.ts +++ b/src/testing/internal/ObservableStream.ts @@ -1,3 +1,7 @@ +import type { Tester } from "@jest/expect-utils"; +import { equals, iterableEquality } from "@jest/expect-utils"; +import { expect } from "@jest/globals"; +import * as matcherUtils from "jest-matcher-utils"; import type { Observable, ObservableSubscription, @@ -31,7 +35,7 @@ export class ObservableStream { take({ timeout = 100 }: TakeOptions = {}) { return Promise.race([ this.reader.read().then((result) => result.value!), - new Promise((_, reject) => { + new Promise>((_, reject) => { setTimeout( reject, timeout, @@ -47,18 +51,57 @@ export class ObservableStream { async takeNext(options?: TakeOptions): Promise { const event = await this.take(options); - expect(event).toEqual({ type: "next", value: expect.anything() }); + validateEquals(event, { type: "next", value: expect.anything() }); return (event as ObservableEvent & { type: "next" }).value; } async takeError(options?: TakeOptions): Promise { const event = await this.take(options); - expect(event).toEqual({ type: "error", error: expect.anything() }); + validateEquals(event, { type: "error", error: expect.anything() }); return (event as ObservableEvent & { type: "error" }).error; } async takeComplete(options?: TakeOptions): Promise { const event = await this.take(options); - expect(event).toEqual({ type: "complete" }); + validateEquals(event, { type: "complete" }); } } + +// Lightweight expect(...).toEqual(...) check that avoids using `expect` so that +// `expect.assertions(num)` does not double count assertions when using the take* +// functions inside of expect(stream).toEmit* matchers. +function validateEquals( + actualEvent: ObservableEvent, + expectedEvent: ObservableEvent +) { + // Uses the same matchers as expect(...).toEqual(...) + // https://github.com/jestjs/jest/blob/611d1a4ba0008d67b5dcda485177f0813b2b573e/packages/expect/src/matchers.ts#L626-L629 + const isEqual = equals(actualEvent, expectedEvent, [ + ...getCustomMatchers(), + iterableEquality, + ]); + + if (isEqual) { + return; + } + + const hint = matcherUtils.matcherHint("toEqual", "stream", "expected"); + + throw new Error( + hint + + "\n\n" + + matcherUtils.printDiffOrStringify( + expectedEvent, + actualEvent, + "Expected", + "Received", + true + ) + ); +} + +function getCustomMatchers(): Array { + // https://github.com/jestjs/jest/blob/611d1a4ba0008d67b5dcda485177f0813b2b573e/packages/expect/src/jestMatchersObject.ts#L141-L143 + const JEST_MATCHERS_OBJECT = Symbol.for("$$jest-matchers-object"); + return (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters; +} diff --git a/src/utilities/observables/__tests__/Concast.ts b/src/utilities/observables/__tests__/Concast.ts index b590cde2fb8..256f6651929 100644 --- a/src/utilities/observables/__tests__/Concast.ts +++ b/src/utilities/observables/__tests__/Concast.ts @@ -1,9 +1,9 @@ -import { itAsync } from "../../../testing/core"; import { Observable, Observer } from "../Observable"; import { Concast, ConcastSourcesIterable } from "../Concast"; +import { ObservableStream } from "../../../testing/internal"; describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { - itAsync("can concatenate other observables", (resolve, reject) => { + it("can concatenate other observables", async () => { const concast = new Concast([ Observable.of(1, 2, 3), Promise.resolve(Observable.of(4, 5)), @@ -12,114 +12,94 @@ describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { Observable.of(11), ]); - const results: number[] = []; - concast.subscribe({ - next(num) { - results.push(num); - }, + const stream = new ObservableStream(concast); + + await expect(stream).toEmitValue(1); + await expect(stream).toEmitValue(2); + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(4); + await expect(stream).toEmitValue(5); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitValue(7); + await expect(stream).toEmitValue(8); + await expect(stream).toEmitValue(9); + await expect(stream).toEmitValue(10); + await expect(stream).toEmitValue(11); + await expect(stream).toComplete(); + + const finalResult = await concast.promise; - error: reject, + expect(finalResult).toBe(11); + }); + it("Can tolerate being completed before input Promise resolves", async () => { + let resolvePromise: (sources: ConcastSourcesIterable) => void; + const delayPromise = new Promise>( + (resolve) => { + resolvePromise = resolve; + } + ); + + const concast = new Concast(delayPromise); + const observer = { + next() { + throw new Error("should not have called observer.next"); + }, + error() { + throw new Error("Should not have called observer.error"); + }, complete() { - concast.promise - .then((finalResult) => { - expect(results).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - expect(finalResult).toBe(11); - resolve(); - }) - .catch(reject); + throw new Error("should not have called observer.complete"); }, - }); + }; + + concast.addObserver(observer); + concast.removeObserver(observer); + + const finalResult = await concast.promise; + expect(finalResult).toBeUndefined(); + + resolvePromise!([]); + const delayedPromiseResult = await delayPromise; + + expect(delayedPromiseResult).toEqual([]); }); - itAsync( - "Can tolerate being completed before input Promise resolves", - (resolve, reject) => { - let resolvePromise: (sources: ConcastSourcesIterable) => void; - const delayPromise = new Promise>( - (resolve) => { - resolvePromise = resolve; - } - ); - - const concast = new Concast(delayPromise); - const observer = { - next() { - reject(new Error("should not have called observer.next")); - }, - error: reject, - complete() { - reject(new Error("should not have called observer.complete")); - }, - }; - - concast.addObserver(observer); - concast.removeObserver(observer); - - return concast.promise - .then((finalResult) => { - expect(finalResult).toBeUndefined(); - resolvePromise([]); - return delayPromise; - }) - .then((delayedPromiseResult) => { - expect(delayedPromiseResult).toEqual([]); - resolve(); - }) - .catch(reject); - } - ); - - itAsync( - "behaves appropriately if unsubscribed before first result", - (resolve, reject) => { - const concast = new Concast([ - new Promise((resolve) => setTimeout(resolve, 100)).then(() => - Observable.of(1, 2, 3) - ), - ]); + it("behaves appropriately if unsubscribed before first result", async () => { + const concast = new Concast([ + new Promise((resolve) => setTimeout(resolve, 100)).then(() => + Observable.of(1, 2, 3) + ), + ]); - const cleanupCounts = { - first: 0, - second: 0, - }; + const cleanupCounts = { + first: 0, + second: 0, + }; - concast.beforeNext(() => { - ++cleanupCounts.first; - }); + concast.beforeNext(() => { + ++cleanupCounts.first; + }); + const stream = new ObservableStream(concast); - const unsubscribe = concast.subscribe({ - next() { - reject("should not have called observer.next"); - }, - error() { - reject("should not have called observer.error"); - }, - complete() { - reject("should not have called observer.complete"); - }, - }); + concast.beforeNext(() => { + ++cleanupCounts.second; + }); - concast.beforeNext(() => { - ++cleanupCounts.second; - }); + // Immediately unsubscribe the observer we just added, triggering + // completion. + stream.unsubscribe(); - // Immediately unsubscribe the observer we just added, triggering - // completion. - unsubscribe.unsubscribe(); - - return concast.promise - .then((finalResult) => { - expect(finalResult).toBeUndefined(); - expect(cleanupCounts).toEqual({ - first: 1, - second: 1, - }); - resolve(); - }) - .catch(reject); - } - ); + const finalResult = await concast.promise; + + expect(finalResult).toBeUndefined(); + expect(cleanupCounts).toEqual({ + first: 1, + second: 1, + }); + + await expect(stream).not.toEmitAnything(); + }); it("concast.beforeNext listeners run before next result/error", () => { const log: Array = [];