diff --git a/.size-limits.json b/.size-limits.json index 5790c408cde..54621796c0c 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41640, + "dist/apollo-client.min.cjs": 41639, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34381 } diff --git a/src/__tests__/local-state/export.ts b/src/__tests__/local-state/export.ts index ea3fb15ae5b..f0e5daf4d60 100644 --- a/src/__tests__/local-state/export.ts +++ b/src/__tests__/local-state/export.ts @@ -2,183 +2,164 @@ import gql from "graphql-tag"; import { print } from "graphql"; import { Observable } from "../../utilities"; -import { itAsync } from "../../testing"; import { ApolloLink } from "../../link/core"; import { ApolloClient } from "../../core"; import { InMemoryCache } from "../../cache"; -import { spyOnConsole } from "../../testing/internal"; +import { ObservableStream, spyOnConsole } from "../../testing/internal"; describe("@client @export tests", () => { - itAsync( - "should not break @client only queries when the @export directive is " + - "used", - (resolve, reject) => { - const query = gql` - { - field @client @export(as: "someVar") - } - `; + it("should not break @client only queries when the @export directive is used", async () => { + const query = gql` + { + field @client @export(as: "someVar") + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link: ApolloLink.empty(), - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + resolvers: {}, + }); - cache.writeQuery({ - query, - data: { field: 1 }, - }); + cache.writeQuery({ + query, + data: { field: 1 }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ field: 1 }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should not break @client only queries when the @export directive is " + - "used on nested fields", - (resolve, reject) => { - const query = gql` - { - car @client { - engine { - torque @export(as: "torque") - } + expect(data).toEqual({ field: 1 }); + }); + + it("should not break @client only queries when the @export directive is used on nested fields", async () => { + const query = gql` + { + car @client { + engine { + torque @export(as: "torque") } } - `; + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link: ApolloLink.empty(), - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + resolvers: {}, + }); - cache.writeQuery({ - query, - data: { - car: { - engine: { - cylinders: 8, - torque: 7200, - __typename: "Engine", - }, - __typename: "Car", + cache.writeQuery({ + query, + data: { + car: { + engine: { + cylinders: 8, + torque: 7200, + __typename: "Engine", }, + __typename: "Car", }, - }); + }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - car: { - engine: { - torque: 7200, - }, - }, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should store the @client field value in the specified @export " + - "variable, and make it available to a subsequent resolver", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) @client - } - `; + expect(data).toEqual({ + car: { + __typename: "Car", + engine: { + __typename: "Engine", + torque: 7200, + }, + }, + }); + }); - const testAuthorId = 100; - const testPostCount = 200; + it("should store the @client field value in the specified @export variable, and make it available to a subsequent resolver", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) @client + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - postCount(_, { authorId }) { - return authorId === testAuthorId ? testPostCount : 0; - }, + const testAuthorId = 100; + const testPostCount = 200; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + postCount(_, { authorId }) { + return authorId === testAuthorId ? testPostCount : 0; }, }, - }); + }, + }); - cache.writeQuery({ - query, - data: { - currentAuthorId: testAuthorId, - }, - }); + cache.writeQuery({ + query, + data: { + currentAuthorId: testAuthorId, + }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - currentAuthorId: testAuthorId, - postCount: testPostCount, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should store the @client nested field value in the specified @export " + - "variable, and make it avilable to a subsequent resolver", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthor @client { - name - authorId @export(as: "authorId") - } - postCount(authorId: $authorId) @client + expect(data).toEqual({ + currentAuthorId: testAuthorId, + postCount: testPostCount, + }); + }); + + it("should store the @client nested field value in the specified @export variable, and make it avilable to a subsequent resolver", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthor @client { + name + authorId @export(as: "authorId") } - `; + postCount(authorId: $authorId) @client + } + `; - const testAuthor = { - name: "John Smith", - authorId: 100, - __typename: "Author", - }; + const testAuthor = { + name: "John Smith", + authorId: 100, + __typename: "Author", + }; - const testPostCount = 200; + const testPostCount = 200; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - postCount(_, { authorId }) { - return authorId === testAuthor.authorId ? testPostCount : 0; - }, + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + postCount(_, { authorId }) { + return authorId === testAuthor.authorId ? testPostCount : 0; }, }, - }); + }, + }); - cache.writeQuery({ - query, - data: { - currentAuthor: testAuthor, - }, - }); + cache.writeQuery({ + query, + data: { + currentAuthor: testAuthor, + }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - currentAuthor: testAuthor, - postCount: testPostCount, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); + + expect({ ...data }).toMatchObject({ + currentAuthor: testAuthor, + postCount: testPostCount, + }); + }); it("should allow @client @export variables to be used with remote queries", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); @@ -233,160 +214,154 @@ describe("@client @export tests", () => { }); }); - itAsync( - "should support @client @export variables that are nested multiple " + - "levels deep", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - appContainer @client { - systemDetails { - currentAuthor { - name - authorId @export(as: "authorId") - } + it("should support @client @export variables that are nested multiple levels deep", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + appContainer @client { + systemDetails { + currentAuthor { + name + authorId @export(as: "authorId") } } - postCount(authorId: $authorId) } - `; + postCount(authorId: $authorId) + } + `; - const appContainer = { - systemDetails: { - currentAuthor: { - name: "John Smith", - authorId: 100, - __typename: "Author", - }, - __typename: "SystemDetails", + const appContainer = { + systemDetails: { + currentAuthor: { + name: "John Smith", + authorId: 100, + __typename: "Author", }, - __typename: "AppContainer", - }; + __typename: "SystemDetails", + }, + __typename: "AppContainer", + }; - const testPostCount = 200; + const testPostCount = 200; - const link = new ApolloLink(() => - Observable.of({ - data: { - postCount: testPostCount, - }, - }) - ); + const link = new ApolloLink(() => + Observable.of({ + data: { + postCount: testPostCount, + }, + }) + ); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { appContainer, }, }); - - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - appContainer, - postCount: testPostCount, - }); - resolve(); - }); } - ); - itAsync( - "should ignore @export directives if not used with @client", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthor { - name - authorId @export(as: "authorId") - } - postCount(authorId: $authorId) - } - `; + const { data } = await client.query({ query }); - const testAuthor = { - name: "John Smith", - authorId: 100, - __typename: "Author", - }; - const testPostCount = 200; + expect(data).toEqual({ + appContainer, + postCount: testPostCount, + }); + }); - const link = new ApolloLink(() => - Observable.of({ - data: { - currentAuthor: testAuthor, - postCount: testPostCount, - }, - }) - ); + it("should ignore @export directives if not used with @client", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthor { + name + authorId @export(as: "authorId") + } + postCount(authorId: $authorId) + } + `; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: {}, - }); + const testAuthor = { + name: "John Smith", + authorId: 100, + __typename: "Author", + }; + const testPostCount = 200; - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ + const link = new ApolloLink(() => + Observable.of({ + data: { currentAuthor: testAuthor, postCount: testPostCount, - }); - resolve(); - }); - } - ); + }, + }) + ); - itAsync( - "should support setting an @client @export variable, loaded from the " + - "cache, on a virtual field that is combined into a remote query.", - (resolve, reject) => { - const query = gql` - query postRequiringReview($reviewerId: Int!) { - postRequiringReview { - id - title - loggedInReviewerId @client @export(as: "reviewerId") - } - reviewerDetails(reviewerId: $reviewerId) { - name - } + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: {}, + }); + + const { data } = await client.query({ query }); + + expect(data).toEqual({ + currentAuthor: testAuthor, + postCount: testPostCount, + }); + }); + + it("should support setting an @client @export variable, loaded from the cache, on a virtual field that is combined into a remote query.", async () => { + const query = gql` + query postRequiringReview($reviewerId: Int!) { + postRequiringReview { + id + title + loggedInReviewerId @client @export(as: "reviewerId") } - `; + reviewerDetails(reviewerId: $reviewerId) { + name + } + } + `; - const postRequiringReview = { - id: 10, - title: "The Local State Conundrum", - __typename: "Post", - }; - const reviewerDetails = { - name: "John Smith", - __typename: "Reviewer", - }; - const loggedInReviewerId = 100; + const postRequiringReview = { + id: 10, + title: "The Local State Conundrum", + __typename: "Post", + }; + const reviewerDetails = { + name: "John Smith", + __typename: "Reviewer", + }; + const loggedInReviewerId = 100; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ reviewerId: loggedInReviewerId }); - return Observable.of({ - data: { - postRequiringReview, - reviewerDetails, - }, - }); - }).setOnError(reject); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ reviewerId: loggedInReviewerId }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, + return Observable.of({ + data: { + postRequiringReview, + reviewerDetails, + }, }); + }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { @@ -397,79 +372,76 @@ describe("@client @export tests", () => { }, }, }); - - return client - .query({ query }) - .then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - postRequiringReview: { - id: postRequiringReview.id, - title: postRequiringReview.title, - loggedInReviewerId, - }, - reviewerDetails, - }); - }) - .then(resolve, reject); } - ); - itAsync( - "should support setting a @client @export variable, loaded via a " + - "local resolver, on a virtual field that is combined into a remote query.", - (resolve, reject) => { - const query = gql` - query postRequiringReview($reviewerId: Int!) { - postRequiringReview { - id - title - currentReviewer @client { - id @export(as: "reviewerId") - } - } - reviewerDetails(reviewerId: $reviewerId) { - name + const { data } = await client.query({ query }); + + expect(data).toEqual({ + postRequiringReview: { + __typename: "Post", + id: postRequiringReview.id, + title: postRequiringReview.title, + loggedInReviewerId, + }, + reviewerDetails, + }); + }); + + it("should support setting a @client @export variable, loaded via a local resolver, on a virtual field that is combined into a remote query.", async () => { + const query = gql` + query postRequiringReview($reviewerId: Int!) { + postRequiringReview { + id + title + currentReviewer @client { + id @export(as: "reviewerId") } } - `; + reviewerDetails(reviewerId: $reviewerId) { + name + } + } + `; - const postRequiringReview = { - id: 10, - title: "The Local State Conundrum", - __typename: "Post", - }; - const reviewerDetails = { - name: "John Smith", - __typename: "Reviewer", - }; - const currentReviewer = { - id: 100, - __typename: "CurrentReviewer", - }; + const postRequiringReview = { + id: 10, + title: "The Local State Conundrum", + __typename: "Post", + }; + const reviewerDetails = { + name: "John Smith", + __typename: "Reviewer", + }; + const currentReviewer = { + id: 100, + __typename: "CurrentReviewer", + }; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ reviewerId: currentReviewer.id }); - return Observable.of({ - data: { - postRequiringReview, - reviewerDetails, - }, - }); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ reviewerId: currentReviewer.id }); + return Observable.of({ + data: { + postRequiringReview, + reviewerDetails, + }, }); - - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: { - Post: { - currentReviewer() { - return currentReviewer; - }, + }); + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: { + Post: { + currentReviewer() { + return currentReviewer; }, }, - }); + }, + }); + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { @@ -478,130 +450,120 @@ describe("@client @export tests", () => { }, }, }); - - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - postRequiringReview: { - id: postRequiringReview.id, - title: postRequiringReview.title, - currentReviewer, - }, - reviewerDetails, - }); - resolve(); - }); } - ); - - itAsync( - "should support combining @client @export variables, calculated by a " + - "local resolver, with remote mutations", - (resolve, reject) => { - const mutation = gql` - mutation upvotePost($postId: Int!) { - topPost @client @export(as: "postId") - upvotePost(postId: $postId) { - title - votes - } + + const { data } = await client.query({ query }); + + expect(data).toMatchObject({ + postRequiringReview: { + id: postRequiringReview.id, + title: postRequiringReview.title, + currentReviewer, + }, + reviewerDetails, + }); + }); + + it("should support combining @client @export variables, calculated by a local resolver, with remote mutations", async () => { + const mutation = gql` + mutation upvotePost($postId: Int!) { + topPost @client @export(as: "postId") + upvotePost(postId: $postId) { + title + votes } - `; + } + `; - const testPostId = 100; - const testPost = { - title: "The Day of the Jackal", - votes: 10, - __typename: "post", - }; + const testPostId = 100; + const testPost = { + title: "The Day of the Jackal", + votes: 10, + __typename: "post", + }; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ postId: testPostId }); - return Observable.of({ - data: { - upvotePost: testPost, - }, - }); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ postId: testPostId }); + return Observable.of({ + data: { + upvotePost: testPost, + }, }); + }); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Mutation: { - topPost() { - return testPostId; - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Mutation: { + topPost() { + return testPostId; }, }, - }); + }, + }); - return client.mutate({ mutation }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - upvotePost: testPost, - }); - resolve(); - }); - } - ); - - itAsync( - "should support combining @client @export variables, calculated by " + - "reading from the cache, with remote mutations", - (resolve, reject) => { - const mutation = gql` - mutation upvotePost($postId: Int!) { - topPost @client @export(as: "postId") - upvotePost(postId: $postId) { - title - votes - } - } - `; + const { data } = await client.mutate({ mutation }); - const testPostId = 100; - const testPost = { - title: "The Day of the Jackal", - votes: 10, - __typename: "post", - }; + expect(data).toEqual({ + topPost: 100, + upvotePost: testPost, + }); + }); - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ postId: testPostId }); - return Observable.of({ - data: { - upvotePost: testPost, - }, - }); - }); + it("should support combining @client @export variables, calculated by reading from the cache, with remote mutations", async () => { + const mutation = gql` + mutation upvotePost($postId: Int!) { + topPost @client @export(as: "postId") + upvotePost(postId: $postId) { + title + votes + } + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const testPostId = 100; + const testPost = { + title: "The Day of the Jackal", + votes: 10, + __typename: "post", + }; - cache.writeQuery({ - query: gql` - { - topPost - } - `, + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ postId: testPostId }); + return Observable.of({ data: { - topPost: testPostId, + upvotePost: testPost, }, }); + }); - return client.mutate({ mutation }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - upvotePost: testPost, - }); - resolve(); - }); - } - ); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + cache.writeQuery({ + query: gql` + { + topPost + } + `, + data: { + topPost: testPostId, + }, + }); + + const { data } = await client.mutate({ mutation }); - it("should not add __typename to @export-ed objects (#4691)", () => { + expect(data).toEqual({ + upvotePost: testPost, + }); + }); + + it("should not add __typename to @export-ed objects (#4691)", async () => { const query = gql` query GetListItems($where: LessonFilter) { currentFilter @client @export(as: "where") { @@ -666,51 +628,50 @@ describe("@client @export tests", () => { }, }); - return client.query({ query }).then((result) => { - expect(result.data).toEqual({ - currentFilter, - ...data, - }); + const result = await client.query({ query }); + + expect(result.data).toEqual({ + currentFilter, + ...data, }); }); - itAsync( - "should use the value of the last @export variable defined, if multiple " + - "variables are defined with the same name", - (resolve, reject) => { - const query = gql` - query reviewerPost($reviewerId: Int!) { - primaryReviewerId @client @export(as: "reviewerId") - secondaryReviewerId @client @export(as: "reviewerId") - post(reviewerId: $reviewerId) { - title - } + it("should use the value of the last @export variable defined, if multiple variables are defined with the same name", async () => { + const query = gql` + query reviewerPost($reviewerId: Int!) { + primaryReviewerId @client @export(as: "reviewerId") + secondaryReviewerId @client @export(as: "reviewerId") + post(reviewerId: $reviewerId) { + title } - `; + } + `; - const post = { - title: "The One Post to Rule Them All", - __typename: "Post", - }; - const primaryReviewerId = 100; - const secondaryReviewerId = 200; + const post = { + title: "The One Post to Rule Them All", + __typename: "Post", + }; + const primaryReviewerId = 100; + const secondaryReviewerId = 200; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ reviewerId: secondaryReviewerId }); - return Observable.of({ - data: { - post, - }, - }); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ reviewerId: secondaryReviewerId }); + return Observable.of({ + data: { + post, + }, }); + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { @@ -718,304 +679,268 @@ describe("@client @export tests", () => { secondaryReviewerId, }, }); - - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - post, - }); - resolve(); - }); } - ); - - it( - "should refetch if an @export variable changes, the current fetch " + - "policy is not cache-only, and the query includes fields that need to " + - "be resolved remotely", - async () => { - using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) - } - `; - const testAuthorId1 = 100; - const testPostCount1 = 200; + const { data } = await client.query({ query }); + + expect(data).toEqual({ + post, + primaryReviewerId, + secondaryReviewerId, + }); + }); + + it("should refetch if an @export variable changes, the current fetch policy is not cache-only, and the query includes fields that need to be resolved remotely", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) + } + `; - const testAuthorId2 = 101; - const testPostCount2 = 201; + const testAuthorId1 = 100; + const testPostCount1 = 200; - let resultCount = 0; + const testAuthorId2 = 101; + const testPostCount2 = 201; - const link = new ApolloLink(() => - Observable.of({ - data: { - postCount: resultCount === 0 ? testPostCount1 : testPostCount2, - }, - }) - ); + let currentAuthorId = testAuthorId1; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const link = new ApolloLink(() => + Observable.of({ + data: { + postCount: + currentAuthorId === testAuthorId1 ? testPostCount1 : testPostCount2, + }, + }) + ); - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId1 }, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - const obs = client.watchQuery({ query }); - obs.subscribe({ - next({ data }) { - if (resultCount === 0) { - expect({ ...data }).toMatchObject({ - currentAuthorId: testAuthorId1, - postCount: testPostCount1, - }); - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId2 }, - }); - } else if (resultCount === 1) { - expect({ ...data }).toMatchObject({ - currentAuthorId: testAuthorId2, - postCount: testPostCount2, - }); - resolve(); - } - resultCount += 1; - }, - }); - }); - } - ); - - it( - "should NOT refetch if an @export variable has not changed, the " + - "current fetch policy is not cache-only, and the query includes fields " + - "that need to be resolved remotely", - async () => { - using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) - } - `; + client.writeQuery({ + query, + data: { currentAuthorId }, + }); - const testAuthorId1 = 100; - const testPostCount1 = 200; + const obs = client.watchQuery({ query }); + const stream = new ObservableStream(obs); - const testPostCount2 = 201; + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId1, + postCount: testPostCount1, + }, + }); - let resultCount = 0; + currentAuthorId = testAuthorId2; + client.writeQuery({ + query, + data: { currentAuthorId }, + }); - let fetchCount = 0; - const link = new ApolloLink(() => { - fetchCount += 1; - return Observable.of({ - data: { - postCount: testPostCount1, - }, - }); - }); + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId2, + postCount: testPostCount2, + }, + }); + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + it("should NOT refetch if an @export variable has not changed, the current fetch policy is not cache-only, and the query includes fields that need to be resolved remotely", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) + } + `; - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId1 }, - }); + const testAuthorId1 = 100; + const testPostCount1 = 200; - const obs = client.watchQuery({ query }); - obs.subscribe({ - next(result) { - if (resultCount === 0) { - expect(fetchCount).toBe(1); - expect(result.data).toMatchObject({ - currentAuthorId: testAuthorId1, - postCount: testPostCount1, - }); - - client.writeQuery({ - query, - variables: { authorId: testAuthorId1 }, - data: { postCount: testPostCount2 }, - }); - } else if (resultCount === 1) { - // Should not have refetched - expect(fetchCount).toBe(1); - resolve(); - } + const testPostCount2 = 201; - resultCount += 1; - }, - }); + let fetchCount = 0; + const link = new ApolloLink(() => { + fetchCount += 1; + return Observable.of({ + data: { + postCount: testPostCount1, + }, }); - } - ); - - itAsync( - "should NOT attempt to refetch over the network if an @export variable " + - "has changed, the current fetch policy is cache-first, and the remote " + - "part of the query (that leverages the @export variable) can be fully " + - "found in the cache.", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) - } - `; + }); - const testAuthorId1 = 1; - const testPostCount1 = 100; + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - const testAuthorId2 = 2; - const testPostCount2 = 200; + client.writeQuery({ + query, + data: { currentAuthorId: testAuthorId1 }, + }); - let fetchCount = 0; - const link = new ApolloLink(() => { - fetchCount += 1; - return Observable.of({ - data: { - postCount: testPostCount1, - }, - }); - }); + const obs = client.watchQuery({ query }); + const stream = new ObservableStream(obs); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId1, + postCount: testPostCount1, + }, + }); + expect(fetchCount).toBe(1); - client.writeQuery({ - query: gql` - { - currentAuthorId - } - `, - data: { currentAuthorId: testAuthorId1 }, - }); + client.writeQuery({ + query, + variables: { authorId: testAuthorId1 }, + data: { postCount: testPostCount2 }, + }); - let resultCount = 0; - const obs = client.watchQuery({ query, fetchPolicy: "cache-first" }); - obs.subscribe({ - next(result) { - if (resultCount === 0) { - // The initial result is fetched over the network. - expect(fetchCount).toBe(1); - expect(result.data).toMatchObject({ - currentAuthorId: testAuthorId1, - postCount: testPostCount1, - }); - - client.writeQuery({ - query, - variables: { authorId: testAuthorId2 }, - data: { postCount: testPostCount2 }, - }); - client.writeQuery({ - query: gql` - { - currentAuthorId - } - `, - data: { currentAuthorId: testAuthorId2 }, - }); - } else if (resultCount === 1) { - // The updated result should not have been fetched over the - // network, as it can be found in the cache. - expect(fetchCount).toBe(1); - expect(result.data).toMatchObject({ - currentAuthorId: testAuthorId2, - postCount: testPostCount2, - }); - resolve(); - } + await expect(stream).toEmitNext(); + expect(fetchCount).toBe(1); + }); + + it("should NOT attempt to refetch over the network if an @export variable has changed, the current fetch policy is cache-first, and the remote part of the query (that leverages the @export variable) can be fully found in the cache.", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) + } + `; + + const testAuthorId1 = 1; + const testPostCount1 = 100; - resultCount += 1; + const testAuthorId2 = 2; + const testPostCount2 = 200; + + let fetchCount = 0; + const link = new ApolloLink(() => { + fetchCount += 1; + return Observable.of({ + data: { + postCount: testPostCount1, }, }); - } - ); + }); - itAsync( - "should update @client @export variables on each broadcast if they've " + - "changed", - (resolve, reject) => { - const cache = new InMemoryCache(); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - const widgetCountQuery = gql` + client.writeQuery({ + query: gql` { - widgetCount @client + currentAuthorId } - `; - cache.writeQuery({ - query: widgetCountQuery, - data: { - widgetCount: 100, - }, - }); + `, + data: { currentAuthorId: testAuthorId1 }, + }); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - doubleWidgets(_, { widgetCount }) { - return widgetCount ? widgetCount * 2 : 0; - }, - }, - }, - }); + const obs = client.watchQuery({ query, fetchPolicy: "cache-first" }); + const stream = new ObservableStream(obs); - const doubleWidgetsQuery = gql` - query DoubleWidgets($widgetCount: Int!) { - widgetCount @client @export(as: "widgetCount") - doubleWidgets(widgetCount: $widgetCount) @client + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId1, + postCount: testPostCount1, + }, + }); + // The initial result is fetched over the network. + expect(fetchCount).toBe(1); + + client.writeQuery({ + query, + variables: { authorId: testAuthorId2 }, + data: { postCount: testPostCount2 }, + }); + client.writeQuery({ + query: gql` + { + currentAuthorId } - `; + `, + data: { currentAuthorId: testAuthorId2 }, + }); - let count = 0; - const obs = client.watchQuery({ query: doubleWidgetsQuery }); - obs.subscribe({ - next({ data }) { - switch (count) { - case 0: - expect(data.widgetCount).toEqual(100); - expect(data.doubleWidgets).toEqual(200); - - client.writeQuery({ - query: widgetCountQuery, - data: { - widgetCount: 500, - }, - }); - break; - case 1: - expect(data.widgetCount).toEqual(500); - expect(data.doubleWidgets).toEqual(1000); - resolve(); - break; - default: - } - count += 1; + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId2, + postCount: testPostCount2, + }, + }); + // The updated result should not have been fetched over the + // network, as it can be found in the cache. + expect(fetchCount).toBe(1); + }); + + it("should update @client @export variables on each broadcast if they've changed", async () => { + const cache = new InMemoryCache(); + + const widgetCountQuery = gql` + { + widgetCount @client + } + `; + cache.writeQuery({ + query: widgetCountQuery, + data: { + widgetCount: 100, + }, + }); + + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + doubleWidgets(_, { widgetCount }) { + return widgetCount ? widgetCount * 2 : 0; + }, }, - }); - } - ); + }, + }); + + const doubleWidgetsQuery = gql` + query DoubleWidgets($widgetCount: Int!) { + widgetCount @client @export(as: "widgetCount") + doubleWidgets(widgetCount: $widgetCount) @client + } + `; + + const obs = client.watchQuery({ query: doubleWidgetsQuery }); + const stream = new ObservableStream(obs); + + await expect(stream).toEmitMatchedValue({ + data: { + widgetCount: 100, + doubleWidgets: 200, + }, + }); + + client.writeQuery({ + query: widgetCountQuery, + data: { + widgetCount: 500, + }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + widgetCount: 500, + doubleWidgets: 1000, + }, + }); + }); }); diff --git a/src/__tests__/local-state/subscriptions.ts b/src/__tests__/local-state/subscriptions.ts index d331cd4fb42..9a3c94edd49 100644 --- a/src/__tests__/local-state/subscriptions.ts +++ b/src/__tests__/local-state/subscriptions.ts @@ -4,10 +4,10 @@ import { Observable } from "../../utilities"; import { ApolloLink } from "../../link/core"; import { ApolloClient } from "../../core"; import { InMemoryCache } from "../../cache"; -import { itAsync } from "../../testing"; +import { ObservableStream } from "../../testing/internal"; describe("Basic functionality", () => { - itAsync("should not break subscriptions", (resolve, reject) => { + it("should not break subscriptions", async () => { const query = gql` subscription { field @@ -28,65 +28,43 @@ describe("Basic functionality", () => { }, }); - let counter = 0; - expect.assertions(2); - client.subscribe({ query }).forEach((item) => { - expect(item).toMatchObject({ data: { field: ++counter } }); - if (counter === 2) { - resolve(); - } - }); + const stream = new ObservableStream(client.subscribe({ query })); + + await expect(stream).toEmitValue({ data: { field: 1 } }); + await expect(stream).toEmitValue({ data: { field: 2 } }); + await expect(stream).toComplete(); }); - itAsync( - "should be able to mix @client fields with subscription results", - (resolve, reject) => { - const query = gql` - subscription { - field - count @client - } - `; + it("should be able to mix @client fields with subscription results", async () => { + const query = gql` + subscription { + field + count @client + } + `; - const link = new ApolloLink(() => - Observable.of({ data: { field: 1 } }, { data: { field: 2 } }) - ); + const link = new ApolloLink(() => + Observable.of({ data: { field: 1 } }, { data: { field: 2 } }) + ); - let subCounter = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Subscription: { - count: () => { - subCounter += 1; - return subCounter; - }, + let subCounter = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Subscription: { + count: () => { + subCounter += 1; + return subCounter; }, }, - }); + }, + }); - expect.assertions(2); - const obs = client.subscribe({ query }); - let resultCounter = 1; - obs.subscribe({ - next(result) { - try { - expect(result).toMatchObject({ - data: { - field: resultCounter, - count: resultCounter, - }, - }); - } catch (error) { - reject(error); - } - resultCounter += 1; - }, - complete() { - resolve(); - }, - }); - } - ); + const stream = new ObservableStream(client.subscribe({ query })); + + await expect(stream).toEmitValue({ data: { field: 1, count: 1 } }); + await expect(stream).toEmitValue({ data: { field: 2, count: 2 } }); + await expect(stream).toComplete(); + }); }); diff --git a/src/__tests__/optimistic.ts b/src/__tests__/optimistic.ts index ed53dc8cf9c..d8d41511a6c 100644 --- a/src/__tests__/optimistic.ts +++ b/src/__tests__/optimistic.ts @@ -10,19 +10,17 @@ import { ApolloCache, MutationQueryReducersMap, TypedDocumentNode, + ApolloError, } from "../core"; import { QueryManager } from "../core/QueryManager"; import { Cache, InMemoryCache } from "../cache"; -import { - Observable, - ObservableSubscription as Subscription, - addTypenameToDocument, -} from "../utilities"; +import { Observable, addTypenameToDocument } from "../utilities"; -import { itAsync, mockSingleLink } from "../testing"; +import { MockedResponse, mockSingleLink } from "../testing"; +import { ObservableStream } from "../testing/internal"; describe("optimistic mutation results", () => { const query = gql` @@ -108,10 +106,7 @@ describe("optimistic mutation results", () => { }, }; - async function setup( - reject: (reason: any) => any, - ...mockedResponses: any[] - ) { + async function setup(...mockedResponses: MockedResponse[]) { const link = mockSingleLink( { request: { query }, @@ -213,223 +208,196 @@ describe("optimistic mutation results", () => { }, }; - itAsync( - "handles a single error for a single mutation", - async (resolve, reject) => { - expect.assertions(6); - const client = await setup(reject, { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }); - try { - const promise = client.mutate({ - mutation, - optimisticResponse, - updateQueries, - }); + it("handles a single error for a single mutation", async () => { + expect.assertions(5); + const client = await setup({ + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }); + const promise = client.mutate({ + mutation, + optimisticResponse, + updateQueries, + }); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" - ); - await promise; - } catch (err) { - expect(err).toBeInstanceOf(Error); - expect((err as Error).message).toBe("forbidden (test error)"); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + } - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); - expect(dataInStore).not.toHaveProperty("Todo99"); - } + await expect(promise).rejects.toThrow( + new ApolloError({ networkError: new Error("forbidden (test error)") }) + ); - resolve(); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); + expect(dataInStore).not.toHaveProperty("Todo99"); } - ); + }); - itAsync( - "handles errors produced by one mutation in a series", - async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }, - { - request: { query: mutation }, - result: mutationResult2, - } - ); + it("handles errors produced by one mutation in a series", async () => { + expect.assertions(12); + const client = await setup( + { + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }, + { + request: { query: mutation }, + result: mutationResult2, + } + ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + const stream = new ObservableStream(client.watchQuery({ query })); - const promise = client - .mutate({ - mutation, - optimisticResponse, - updateQueries, - }) - .catch((err: any) => { - // it is ok to fail here - expect(err).toBeInstanceOf(Error); - expect(err.message).toBe("forbidden (test error)"); - return null; - }); + await expect(stream).toEmitNext(); - const promise2 = client.mutate({ + const promise = client + .mutate({ mutation, - optimisticResponse: optimisticResponse2, + optimisticResponse, updateQueries, + }) + .catch((err: any) => { + // it is ok to fail here + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe("forbidden (test error)"); + return null; }); + const promise2 = client.mutate({ + mutation, + optimisticResponse: optimisticResponse2, + updateQueries, + }); + + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + expect((dataInStore["Todo66"] as any).text).toBe( + "Optimistically generated 2" + ); + + await Promise.all([promise, promise2]); + + stream.unsubscribe(); + + { const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect(dataInStore).not.toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") ); - expect((dataInStore["Todo66"] as any).text).toBe( - "Optimistically generated 2" + expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( + makeReference("Todo99") ); + } + }); - await Promise.all([promise, promise2]); - - subscriptionHandle!.unsubscribe(); + it("can run 2 mutations concurrently and handles all intermediate states well", async () => { + expect.assertions(36); + function checkBothMutationsAreApplied( + expectedText1: any, + expectedText2: any + ) { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect(dataInStore).toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + // can be removed once @types/chai adds deepInclude + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") + ); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo99") + ); + expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); + expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); + } + const client = await setup( { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect(dataInStore).not.toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( - makeReference("Todo99") - ); - resolve(); + request: { query: mutation }, + result: mutationResult, + }, + { + request: { query: mutation }, + result: mutationResult2, + // make sure it always happens later + delay: 100, } - } - ); + ); + const stream = new ObservableStream(client.watchQuery({ query })); - itAsync( - "can run 2 mutations concurrently and handles all intermediate states well", - async (resolve, reject) => { - expect.assertions(34); - function checkBothMutationsAreApplied( - expectedText1: any, - expectedText2: any - ) { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect(dataInStore).toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - // can be removed once @types/chai adds deepInclude - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo99") + await expect(stream).toEmitNext(); + + const queryManager: QueryManager = (client as any).queryManager; + + const promise = client + .mutate({ + mutation, + optimisticResponse, + updateQueries, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Optimistically generated 2" ); - expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); - expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); - } - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - result: mutationResult, - }, - { - request: { query: mutation }, - result: mutationResult2, - // make sure it always happens later - delay: 100, - } - ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); - const queryManager: QueryManager = (client as any).queryManager; + // @ts-ignore + const latestState = queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(true); - const promise = client - .mutate({ - mutation, - optimisticResponse, - updateQueries, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Optimistically generated 2" - ); - - // @ts-ignore - const latestState = queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(true); - - return res; - }); + return res; + }); - const promise2 = client - .mutate({ - mutation, - optimisticResponse: optimisticResponse2, - updateQueries, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); - - // @ts-ignore - const latestState = queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(false); - - return res; - }); + const promise2 = client + .mutate({ + mutation, + optimisticResponse: optimisticResponse2, + updateQueries, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); - // @ts-ignore - const mutationsState = queryManager.mutationStore!; - expect(mutationsState[1].loading).toBe(true); - expect(mutationsState[2].loading).toBe(true); + // @ts-ignore + const latestState = queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(false); - checkBothMutationsAreApplied( - "Optimistically generated", - "Optimistically generated 2" - ); + return res; + }); - await Promise.all([promise, promise2]); + // @ts-ignore + const mutationsState = queryManager.mutationStore!; + expect(mutationsState[1].loading).toBe(true); + expect(mutationsState[2].loading).toBe(true); - subscriptionHandle!.unsubscribe(); - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); + checkBothMutationsAreApplied( + "Optimistically generated", + "Optimistically generated 2" + ); - resolve(); - } - ); + await Promise.all([promise, promise2]); + + stream.unsubscribe(); + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); + }); }); describe("with `update`", () => { @@ -464,225 +432,195 @@ describe("optimistic mutation results", () => { }); }; - itAsync( - "handles a single error for a single mutation", - async (resolve, reject) => { - expect.assertions(6); - - const client = await setup(reject, { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }); + it("handles a single error for a single mutation", async () => { + expect.assertions(5); - try { - const promise = client.mutate({ - mutation, - optimisticResponse, - update, - }); + const client = await setup({ + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" - ); + const promise = client.mutate({ + mutation, + optimisticResponse, + update, + }); - await promise; - } catch (err) { - expect(err).toBeInstanceOf(Error); - expect((err as Error).message).toBe("forbidden (test error)"); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + } - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); - expect(dataInStore).not.toHaveProperty("Todo99"); - } + await expect(promise).rejects.toThrow( + new ApolloError({ networkError: new Error("forbidden (test error)") }) + ); - resolve(); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); + expect(dataInStore).not.toHaveProperty("Todo99"); } - ); + }); - itAsync( - "handles errors produced by one mutation in a series", - async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }, - { - request: { query: mutation }, - result: mutationResult2, - } - ); + it("handles errors produced by one mutation in a series", async () => { + expect.assertions(12); + const client = await setup( + { + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }, + { + request: { query: mutation }, + result: mutationResult2, + } + ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + const stream = new ObservableStream(client.watchQuery({ query })); - const promise = client - .mutate({ - mutation, - optimisticResponse, - update, - }) - .catch((err: any) => { - // it is ok to fail here - expect(err).toBeInstanceOf(Error); - expect(err.message).toBe("forbidden (test error)"); - return null; - }); + await expect(stream).toEmitNext(); - const promise2 = client.mutate({ + const promise = client + .mutate({ mutation, - optimisticResponse: optimisticResponse2, + optimisticResponse, update, + }) + .catch((err: any) => { + // it is ok to fail here + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe("forbidden (test error)"); + return null; }); + const promise2 = client.mutate({ + mutation, + optimisticResponse: optimisticResponse2, + update, + }); + + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + expect((dataInStore["Todo66"] as any).text).toBe( + "Optimistically generated 2" + ); + + await Promise.all([promise, promise2]); + + stream.unsubscribe(); + { const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect(dataInStore).not.toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") ); - expect((dataInStore["Todo66"] as any).text).toBe( - "Optimistically generated 2" + expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( + makeReference("Todo99") ); + } + }); - await Promise.all([promise, promise2]); + it("can run 2 mutations concurrently and handles all intermediate states well", async () => { + expect.assertions(36); + function checkBothMutationsAreApplied( + expectedText1: any, + expectedText2: any + ) { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect(dataInStore).toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") + ); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo99") + ); + expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); + expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); + } - subscriptionHandle!.unsubscribe(); + const client = await setup( { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect(dataInStore).not.toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( - makeReference("Todo99") - ); - resolve(); + request: { query: mutation }, + result: mutationResult, + }, + { + request: { query: mutation }, + result: mutationResult2, + // make sure it always happens later + delay: 100, } - } - ); + ); + const stream = new ObservableStream(client.watchQuery({ query })); - itAsync( - "can run 2 mutations concurrently and handles all intermediate states well", - async (resolve, reject) => { - expect.assertions(34); - function checkBothMutationsAreApplied( - expectedText1: any, - expectedText2: any - ) { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect(dataInStore).toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo99") + await expect(stream).toEmitNext(); + + const promise = client + .mutate({ + mutation, + optimisticResponse, + update, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Optimistically generated 2" ); - expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); - expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); - } - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - result: mutationResult, - }, - { - request: { query: mutation }, - result: mutationResult2, - // make sure it always happens later - delay: 100, - } - ); + // @ts-ignore + const latestState = client.queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(true); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); + return res; }); - const promise = client - .mutate({ - mutation, - optimisticResponse, - update, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Optimistically generated 2" - ); - - // @ts-ignore - const latestState = client.queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(true); - - return res; - }); + const promise2 = client + .mutate({ + mutation, + optimisticResponse: optimisticResponse2, + update, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); - const promise2 = client - .mutate({ - mutation, - optimisticResponse: optimisticResponse2, - update, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); - - // @ts-ignore - const latestState = client.queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(false); - - return res; - }); + // @ts-ignore + const latestState = client.queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(false); - // @ts-ignore - const mutationsState = client.queryManager.mutationStore!; - expect(mutationsState[1].loading).toBe(true); - expect(mutationsState[2].loading).toBe(true); + return res; + }); - checkBothMutationsAreApplied( - "Optimistically generated", - "Optimistically generated 2" - ); + // @ts-ignore + const mutationsState = client.queryManager.mutationStore!; + expect(mutationsState[1].loading).toBe(true); + expect(mutationsState[2].loading).toBe(true); - await Promise.all([promise, promise2]); + checkBothMutationsAreApplied( + "Optimistically generated", + "Optimistically generated 2" + ); - subscriptionHandle!.unsubscribe(); - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); + await Promise.all([promise, promise2]); - resolve(); - } - ); + stream.unsubscribe(); + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); + }); }); }); @@ -752,39 +690,34 @@ describe("optimistic mutation results", () => { } `; - itAsync( - "client.readQuery should read the optimistic response of a mutation " + - "only when update function is called optimistically", - (resolve, reject) => { - return setup(reject, { - request: { query: todoListMutation }, - result: todoListMutationResult, - }) - .then((client) => { - let updateCount = 0; - return client.mutate({ - mutation: todoListMutation, - optimisticResponse: todoListOptimisticResponse, - update: (proxy: any, mResult: any) => { - ++updateCount; - const data = proxy.readQuery({ query: todoListQuery }); - const readText = data.todoList.todos[0].text; - if (updateCount === 1) { - const optimisticText = - todoListOptimisticResponse.createTodo.todos[0].text; - expect(readText).toEqual(optimisticText); - } else if (updateCount === 2) { - const incomingText = mResult.data.createTodo.todos[0].text; - expect(readText).toEqual(incomingText); - } else { - reject("too many update calls"); - } - }, - }); - }) - .then(resolve, reject); - } - ); + it("client.readQuery should read the optimistic response of a mutation only when update function is called optimistically", async () => { + expect.assertions(2); + const client = await setup({ + request: { query: todoListMutation }, + result: todoListMutationResult, + }); + + let updateCount = 0; + await client.mutate({ + mutation: todoListMutation, + optimisticResponse: todoListOptimisticResponse, + update: (proxy: any, mResult: any) => { + ++updateCount; + const data = proxy.readQuery({ query: todoListQuery }); + const readText = data.todoList.todos[0].text; + if (updateCount === 1) { + const optimisticText = + todoListOptimisticResponse.createTodo.todos[0].text; + expect(readText).toEqual(optimisticText); + } else if (updateCount === 2) { + const incomingText = mResult.data.createTodo.todos[0].text; + expect(readText).toEqual(incomingText); + } else { + throw new Error("too many update calls"); + } + }, + }); + }); const todoListFragment = gql` fragment todoList on TodoList { @@ -797,79 +730,67 @@ describe("optimistic mutation results", () => { } `; - itAsync( - "should read the optimistic response of a mutation when making an " + - "ApolloClient.readFragment() call, if the `optimistic` param is set " + - "to true", - (resolve, reject) => { - return setup(reject, { - request: { query: todoListMutation }, - result: todoListMutationResult, - }) - .then((client) => { - let updateCount = 0; - return client.mutate({ - mutation: todoListMutation, - optimisticResponse: todoListOptimisticResponse, - update: (proxy: any, mResult: any) => { - ++updateCount; - const data: any = proxy.readFragment( - { - id: "TodoList5", - fragment: todoListFragment, - }, - true - ); - if (updateCount === 1) { - expect(data.todos[0].text).toEqual( - todoListOptimisticResponse.createTodo.todos[0].text - ); - } else if (updateCount === 2) { - expect(data.todos[0].text).toEqual( - mResult.data.createTodo.todos[0].text - ); - expect(data.todos[0].text).toEqual( - todoListMutationResult.data.createTodo.todos[0].text - ); - } else { - reject("too many update calls"); - } - }, - }); - }) - .then(resolve, reject); - } - ); + it("should read the optimistic response of a mutation when making an ApolloClient.readFragment() call, if the `optimistic` param is set to true", async () => { + expect.assertions(3); + const client = await setup({ + request: { query: todoListMutation }, + result: todoListMutationResult, + }); - itAsync( - "should not read the optimistic response of a mutation when making " + - "an ApolloClient.readFragment() call, if the `optimistic` param is " + - "set to false", - (resolve, reject) => { - return setup(reject, { - request: { query: todoListMutation }, - result: todoListMutationResult, - }) - .then((client) => { - return client.mutate({ - mutation: todoListMutation, - optimisticResponse: todoListOptimisticResponse, - update: (proxy: any, mResult: any) => { - const incomingText = mResult.data.createTodo.todos[0].text; - const data: any = proxy.readFragment( - { - id: "TodoList5", - fragment: todoListFragment, - }, - false - ); - expect(data.todos[0].text).toEqual(incomingText); - }, - }); - }) - .then(resolve, reject); - } - ); + let updateCount = 0; + await client.mutate({ + mutation: todoListMutation, + optimisticResponse: todoListOptimisticResponse, + update: (proxy: any, mResult: any) => { + ++updateCount; + const data: any = proxy.readFragment( + { + id: "TodoList5", + fragment: todoListFragment, + }, + true + ); + if (updateCount === 1) { + expect(data.todos[0].text).toEqual( + todoListOptimisticResponse.createTodo.todos[0].text + ); + } else if (updateCount === 2) { + expect(data.todos[0].text).toEqual( + mResult.data.createTodo.todos[0].text + ); + expect(data.todos[0].text).toEqual( + todoListMutationResult.data.createTodo.todos[0].text + ); + } else { + throw new Error("too many update calls"); + } + }, + }); + }); + + it("should not read the optimistic response of a mutation when making an ApolloClient.readFragment() call, if the `optimistic` param is set to false", async () => { + expect.assertions(2); + const client = await setup({ + request: { query: todoListMutation }, + result: todoListMutationResult, + }); + + await client.mutate({ + mutation: todoListMutation, + optimisticResponse: todoListOptimisticResponse, + update: (proxy: any, mResult: any) => { + const incomingText = mResult.data.createTodo.todos[0].text; + const data: any = proxy.readFragment( + { + id: "TodoList5", + fragment: todoListFragment, + }, + false + ); + expect(data.todos[0].text).toEqual(incomingText); + }, + }); + }); }); describe("passing a function to optimisticResponse", () => { @@ -909,187 +830,157 @@ describe("optimistic mutation results", () => { }, }); - itAsync( - "will use a passed variable in optimisticResponse", - async (resolve, reject) => { - expect.assertions(6); - let subscriptionHandle: Subscription; - const client = await setup(reject, { - request: { query: mutation, variables }, - result: mutationResult, - }); - - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); - - const promise = client.mutate({ - mutation, - variables, - optimisticResponse, - update: (proxy: any, mResult: any) => { - expect(mResult.data.createTodo.id).toBe("99"); - - const id = "TodoList5"; - const fragment = gql` - fragment todoList on TodoList { - todos { - id - text - completed - __typename - } - } - `; - - const data: any = proxy.readFragment({ id, fragment }); + it("will use a passed variable in optimisticResponse", async () => { + expect.assertions(8); + const client = await setup({ + request: { query: mutation, variables }, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - proxy.writeFragment({ - data: { - ...data, - todos: [mResult.data.createTodo, ...data.todos], - }, - id, - fragment, - }); - }, - }); + await expect(stream).toEmitNext(); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); - expect((dataInStore["Todo99"] as any).text).toEqual( - "Optimistically generated from variables" - ); + const promise = client.mutate({ + mutation, + variables, + optimisticResponse, + update: (proxy: any, mResult: any) => { + expect(mResult.data.createTodo.id).toBe("99"); - await promise; + const id = "TodoList5"; + const fragment = gql` + fragment todoList on TodoList { + todos { + id + text + completed + __typename + } + } + `; - const newResult: any = await client.query({ query }); + const data: any = proxy.readFragment({ id, fragment }); - subscriptionHandle!.unsubscribe(); - // There should be one more todo item than before - expect(newResult.data.todoList.todos.length).toEqual(4); + proxy.writeFragment({ + data: { + ...data, + todos: [mResult.data.createTodo, ...data.todos], + }, + id, + fragment, + }); + }, + }); - // Since we used `prepend` it should be at the front - expect(newResult.data.todoList.todos[0].text).toEqual( - "This one was created with a mutation." - ); + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); + expect((dataInStore["Todo99"] as any).text).toEqual( + "Optimistically generated from variables" + ); - resolve(); - } - ); + await promise; - itAsync( - "will not update optimistically if optimisticResponse returns IGNORE sentinel object", - async (resolve, reject) => { - expect.assertions(5); + const newResult: any = await client.query({ query }); - let subscriptionHandle: Subscription; + stream.unsubscribe(); + // There should be one more todo item than before + expect(newResult.data.todoList.todos.length).toEqual(4); - const client = await setup(reject, { - request: { query: mutation, variables }, - result: mutationResult, - }); + // Since we used `prepend` it should be at the front + expect(newResult.data.todoList.todos[0].text).toEqual( + "This one was created with a mutation." + ); + }); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + it("will not update optimistically if optimisticResponse returns IGNORE sentinel object", async () => { + expect.assertions(7); - const id = "TodoList5"; - const isTodoList = ( - list: unknown - ): list is { todos: { text: string }[] } => - typeof initialList === "object" && - initialList !== null && - "todos" in initialList && - Array.isArray(initialList.todos); + const client = await setup({ + request: { query: mutation, variables }, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - const initialList = client.cache.extract(true)[id]; + await expect(stream).toEmitNext(); - if (!isTodoList(initialList)) { - reject(new Error("Expected TodoList")); - return; - } + const id = "TodoList5"; + const isTodoList = ( + list: unknown + ): list is { todos: { text: string }[] } => + typeof initialList === "object" && + initialList !== null && + "todos" in initialList && + Array.isArray(initialList.todos); - expect(initialList.todos.length).toEqual(3); + const initialList = client.cache.extract(true)[id]; - const promise = client.mutate({ - mutation, - variables, - optimisticResponse: (vars, { IGNORE }) => { - return IGNORE; - }, - update: (proxy: any, mResult: any) => { - expect(mResult.data.createTodo.id).toBe("99"); - - const fragment = gql` - fragment todoList on TodoList { - todos { - id - text - completed - __typename - } - } - `; + if (!isTodoList(initialList)) { + throw new Error("Expected TodoList"); + } - const data: any = proxy.readFragment({ id, fragment }); + expect(initialList.todos.length).toEqual(3); - proxy.writeFragment({ - data: { - ...data, - todos: [mResult.data.createTodo, ...data.todos], - }, - id, - fragment, - }); - }, - }); + const promise = client.mutate({ + mutation, + variables, + optimisticResponse: (vars, { IGNORE }) => { + return IGNORE; + }, + update: (proxy: any, mResult: any) => { + expect(mResult.data.createTodo.id).toBe("99"); - const list = client.cache.extract(true)[id]; + const fragment = gql` + fragment todoList on TodoList { + todos { + id + text + completed + __typename + } + } + `; - if (!isTodoList(list)) { - reject(new Error("Expected TodoList")); - return; - } + const data: any = proxy.readFragment({ id, fragment }); - expect(list.todos.length).toEqual(3); + proxy.writeFragment({ + data: { + ...data, + todos: [mResult.data.createTodo, ...data.todos], + }, + id, + fragment, + }); + }, + }); - await promise; + const list = client.cache.extract(true)[id]; - const result = await client.query({ query }); + if (!isTodoList(list)) { + throw new Error("Expected TodoList"); + } - subscriptionHandle!.unsubscribe(); + expect(list.todos.length).toEqual(3); - const newList = result.data.todoList; + await promise; - if (!isTodoList(newList)) { - reject(new Error("Expected TodoList")); - return; - } + const result = await client.query({ query }); - // There should be one more todo item than before - expect(newList.todos.length).toEqual(4); + stream.unsubscribe(); - // Since we used `prepend` it should be at the front - expect(newList.todos[0].text).toBe( - "This one was created with a mutation." - ); + const newList = result.data.todoList; - resolve(); + if (!isTodoList(newList)) { + throw new Error("Expected TodoList"); } - ); + + // There should be one more todo item than before + expect(newList.todos.length).toEqual(4); + + // Since we used `prepend` it should be at the front + expect(newList.todos[0].text).toBe( + "This one was created with a mutation." + ); + }); it("allows IgnoreModifier as return value when inferring from a TypedDocumentNode mutation", () => { const mutation: TypedDocumentNode<{ bar: string }> = gql` @@ -1176,72 +1067,57 @@ describe("optimistic mutation results", () => { }, }; - itAsync( - "will insert a single itemAsync to the beginning", - async (resolve, reject) => { - expect.assertions(7); - let subscriptionHandle: Subscription; - const client = await setup(reject, { - request: { query: mutation }, - result: mutationResult, - }); + it("will insert a single itemAsync to the beginning", async () => { + expect.assertions(9); + const client = await setup({ + request: { query: mutation }, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); - const promise = client.mutate({ - mutation, - optimisticResponse, - updateQueries: { - todoList(prev: any, options: any) { - const mResult = options.mutationResult as any; - expect(mResult.data.createTodo.id).toEqual("99"); - return { - ...prev, - todoList: { - ...prev.todoList, - todos: [mResult.data.createTodo, ...prev.todoList.todos], - }, - }; - }, + const promise = client.mutate({ + mutation, + optimisticResponse, + updateQueries: { + todoList(prev: any, options: any) { + const mResult = options.mutationResult as any; + expect(mResult.data.createTodo.id).toEqual("99"); + return { + ...prev, + todoList: { + ...prev.todoList, + todos: [mResult.data.createTodo, ...prev.todoList.todos], + }, + }; }, - }); - - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); - expect((dataInStore["Todo99"] as any).text).toEqual( - "Optimistically generated" - ); + }, + }); - await promise; + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); + expect((dataInStore["Todo99"] as any).text).toEqual( + "Optimistically generated" + ); - const newResult: any = await client.query({ query }); + await promise; - subscriptionHandle!.unsubscribe(); - // There should be one more todo item than before - expect(newResult.data.todoList.todos.length).toEqual(4); + const newResult: any = await client.query({ query }); - // Since we used `prepend` it should be at the front - expect(newResult.data.todoList.todos[0].text).toEqual( - "This one was created with a mutation." - ); + stream.unsubscribe(); + // There should be one more todo item than before + expect(newResult.data.todoList.todos.length).toEqual(4); - resolve(); - } - ); + // Since we used `prepend` it should be at the front + expect(newResult.data.todoList.todos[0].text).toEqual( + "This one was created with a mutation." + ); + }); - itAsync("two array insert like mutations", async (resolve, reject) => { - expect.assertions(9); - let subscriptionHandle: Subscription; + it("two array insert like mutations", async () => { + expect.assertions(11); const client = await setup( - reject, { request: { query: mutation }, result: mutationResult, @@ -1252,16 +1128,9 @@ describe("optimistic mutation results", () => { delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const updateQueries = { todoList: (prev, options) => { @@ -1317,7 +1186,7 @@ describe("optimistic mutation results", () => { const newResult: any = await client.query({ query }); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); // There should be one more todo item than before expect(newResult.data.todoList.todos.length).toEqual(5); @@ -1326,15 +1195,11 @@ describe("optimistic mutation results", () => { expect(newResult.data.todoList.todos[1].text).toEqual( "This one was created with a mutation." ); - - resolve(); }); - itAsync("two mutations, one fails", async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; + it("two mutations, one fails", async () => { + expect.assertions(12); const client = await setup( - reject, { request: { query: mutation }, error: new Error("forbidden (test error)"), @@ -1351,16 +1216,9 @@ describe("optimistic mutation results", () => { // delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const updateQueries = { todoList: (prev, options) => { @@ -1405,7 +1263,7 @@ describe("optimistic mutation results", () => { await Promise.all([promise, promise2]); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); { const dataInStore = (client.cache as InMemoryCache).extract(true); expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); @@ -1417,11 +1275,10 @@ describe("optimistic mutation results", () => { expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( makeReference("Todo99") ); - resolve(); } }); - itAsync("will handle dependent updates", async (resolve, reject) => { + it("will handle dependent updates", async () => { expect.assertions(1); const link = mockSingleLink( { @@ -1438,7 +1295,7 @@ describe("optimistic mutation results", () => { result: mutationResult2, delay: 20, } - ).setOnError(reject); + ); const customOptimisticResponse1 = { __typename: "Mutation", @@ -1502,13 +1359,13 @@ describe("optimistic mutation results", () => { // https://github.com/apollographql/apollo-client/issues/3723 await new Promise((resolve) => setTimeout(resolve)); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse1, updateQueries, }); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse2, updateQueries, @@ -1536,8 +1393,6 @@ describe("optimistic mutation results", () => { ...defaultTodos, ], ]); - - resolve(); }); }); @@ -1596,82 +1451,67 @@ describe("optimistic mutation results", () => { }, }; - itAsync( - "will insert a single itemAsync to the beginning", - async (resolve, reject) => { - expect.assertions(6); - let subscriptionHandle: Subscription; - const client = await setup(reject, { - request: { query: mutation }, - delay: 300, - result: mutationResult, - }); + it("will insert a single itemAsync to the beginning", async () => { + expect.assertions(8); + const client = await setup({ + request: { query: mutation }, + delay: 300, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); - let firstTime = true; - let before = Date.now(); - const promise = client.mutate({ - mutation, - optimisticResponse, - update: (proxy: any, mResult: any) => { - const after = Date.now(); - const duration = after - before; - if (firstTime) { - expect(duration < 300).toBe(true); - firstTime = false; - } else { - expect(duration > 300).toBe(true); - } - let data = proxy.readQuery({ query }); + let firstTime = true; + let before = Date.now(); + const promise = client.mutate({ + mutation, + optimisticResponse, + update: (proxy: any, mResult: any) => { + const after = Date.now(); + const duration = after - before; + if (firstTime) { + expect(duration < 300).toBe(true); + firstTime = false; + } else { + expect(duration > 300).toBe(true); + } + let data = proxy.readQuery({ query }); - proxy.writeQuery({ - query, - data: { - ...data, - todoList: { - ...data.todoList, - todos: [mResult.data.createTodo, ...data.todoList.todos], - }, + proxy.writeQuery({ + query, + data: { + ...data, + todoList: { + ...data.todoList, + todos: [mResult.data.createTodo, ...data.todoList.todos], }, - }); - }, - }); + }, + }); + }, + }); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" - ); - await promise; - await client.query({ query }).then((newResult: any) => { - subscriptionHandle!.unsubscribe(); - // There should be one more todo item than before - expect(newResult.data.todoList.todos.length).toBe(4); - - // Since we used `prepend` it should be at the front - expect(newResult.data.todoList.todos[0].text).toBe( - "This one was created with a mutation." - ); - }); + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + await promise; + const newResult = await client.query({ query }); - resolve(); - } - ); + stream.unsubscribe(); + // There should be one more todo item than before + expect(newResult.data.todoList.todos.length).toBe(4); - itAsync("two array insert like mutations", async (resolve, reject) => { - expect.assertions(9); - let subscriptionHandle: Subscription; + // Since we used `prepend` it should be at the front + expect(newResult.data.todoList.todos[0].text).toBe( + "This one was created with a mutation." + ); + }); + + it("two array insert like mutations", async () => { + expect.assertions(11); const client = await setup( - reject, { request: { query: mutation }, result: mutationResult, @@ -1682,16 +1522,9 @@ describe("optimistic mutation results", () => { delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const update = (proxy: any, mResult: any) => { const data: any = proxy.readFragment({ @@ -1765,7 +1598,7 @@ describe("optimistic mutation results", () => { const newResult: any = await client.query({ query }); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); // There should be one more todo item than before expect(newResult.data.todoList.todos.length).toBe(5); @@ -1774,15 +1607,11 @@ describe("optimistic mutation results", () => { expect(newResult.data.todoList.todos[1].text).toBe( "This one was created with a mutation." ); - - resolve(); }); - itAsync("two mutations, one fails", async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; + it("two mutations, one fails", async () => { + expect.assertions(12); const client = await setup( - reject, { request: { query: mutation }, error: new Error("forbidden (test error)"), @@ -1799,16 +1628,9 @@ describe("optimistic mutation results", () => { // delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const update = (proxy: any, mResult: any) => { const data: any = proxy.readFragment({ @@ -1873,7 +1695,7 @@ describe("optimistic mutation results", () => { await Promise.all([promise, promise2]); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); { const dataInStore = (client.cache as InMemoryCache).extract(true); expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); @@ -1885,11 +1707,10 @@ describe("optimistic mutation results", () => { expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( makeReference("Todo99") ); - resolve(); } }); - itAsync("will handle dependent updates", async (resolve, reject) => { + it("will handle dependent updates", async () => { expect.assertions(1); const link = mockSingleLink( { @@ -1906,7 +1727,7 @@ describe("optimistic mutation results", () => { result: mutationResult2, delay: 20, } - ).setOnError(reject); + ); const customOptimisticResponse1 = { __typename: "Mutation", @@ -1983,13 +1804,13 @@ describe("optimistic mutation results", () => { await new Promise((resolve) => setTimeout(resolve)); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse1, update, }); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse2, update, @@ -2016,11 +1837,9 @@ describe("optimistic mutation results", () => { ...defaultTodos, ], ]); - - resolve(); }); - itAsync("final update ignores optimistic data", (resolve, reject) => { + it("final update ignores optimistic data", async () => { const cache = new InMemoryCache(); const client = new ApolloClient({ cache, @@ -2143,209 +1962,190 @@ describe("optimistic mutation results", () => { const optimisticItem = makeItem("optimistic"); const mutationItem = makeItem("mutation"); - const wrapReject = ( - fn: (...args: TArgs) => TResult - ): typeof fn => { - return function (this: unknown, ...args: TArgs) { - try { - return fn.apply(this, args); - } catch (e) { - reject(e); - throw e; - } - }; - }; + const result = await client.mutate({ + mutation, + optimisticResponse: { + addItem: optimisticItem, + }, + variables: { + item: mutationItem, + }, + update: (cache, mutationResult) => { + ++updateCount; + if (updateCount === 1) { + expect(mutationResult).toEqual({ + data: { + addItem: optimisticItem, + }, + }); - return client - .mutate({ - mutation, - optimisticResponse: { - addItem: optimisticItem, - }, - variables: { - item: mutationItem, - }, - update: wrapReject((cache, mutationResult) => { - ++updateCount; - if (updateCount === 1) { - expect(mutationResult).toEqual({ - data: { - addItem: optimisticItem, + append(cache, optimisticItem); + + const expected = { + ROOT_QUERY: { + __typename: "Query", + items: [manualItem1, manualItem2, optimisticItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + // Although ROOT_MUTATION field data gets removed immediately + // after the mutation finishes, it is still temporarily visible + // to the update function. + 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': { + __typename: "Item", + text: "optimistic 3", }, - }); + }, + }; + + // Since we're in an optimistic update function, reading + // non-optimistically still returns optimistic data. + expect(cache.extract(false)).toEqual(expected); + expect(cache.extract(true)).toEqual(expected); + } else if (updateCount === 2) { + expect(mutationResult).toEqual({ + data: { + addItem: mutationItem, + }, + }); - append(cache, optimisticItem); + append(cache, mutationItem); - const expected = { - ROOT_QUERY: { - __typename: "Query", - items: [manualItem1, manualItem2, optimisticItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - // Although ROOT_MUTATION field data gets removed immediately - // after the mutation finishes, it is still temporarily visible - // to the update function. - 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': - { - __typename: "Item", - text: "optimistic 3", - }, - }, - }; - - // Since we're in an optimistic update function, reading - // non-optimistically still returns optimistic data. - expect(cache.extract(false)).toEqual(expected); - expect(cache.extract(true)).toEqual(expected); - } else if (updateCount === 2) { - expect(mutationResult).toEqual({ - data: { - addItem: mutationItem, + const expected = { + ROOT_QUERY: { + __typename: "Query", + items: [mutationItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': { + __typename: "Item", + text: "mutation 4", }, - }); + }, + }; + + // Since we're in the final (non-optimistic) update function, + // optimistic data is invisible, even if we try to read + // optimistically. + expect(cache.extract(false)).toEqual(expected); + expect(cache.extract(true)).toEqual(expected); + } else { + throw new Error("too many updates"); + } + }, + }); - append(cache, mutationItem); + expect(result).toEqual({ + data: { + addItem: mutationItem, + }, + }); - const expected = { - ROOT_QUERY: { - __typename: "Query", - items: [mutationItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': - { - __typename: "Item", - text: "mutation 4", - }, - }, - }; - - // Since we're in the final (non-optimistic) update function, - // optimistic data is invisible, even if we try to read - // optimistically. - expect(cache.extract(false)).toEqual(expected); - expect(cache.extract(true)).toEqual(expected); - } else { - throw new Error("too many updates"); - } - }), - }) - .then((result) => { - expect(result).toEqual({ - data: { - addItem: mutationItem, - }, - }); + // Only the final update function ever touched non-optimistic + // cache data. + expect(cache.extract(false)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + items: [mutationItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + }, + }); - // Only the final update function ever touched non-optimistic - // cache data. - expect(cache.extract(false)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - items: [mutationItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - }, - }); + // Now that the mutation is finished, reading optimistically from + // the cache should return the manually added items again. + expect(cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + items: [ + // If we wanted to keep optimistic data as up-to-date as + // possible, we could rerun all optimistic transactions + // after writing to the root (non-optimistic) layer of the + // cache, which would result in mutationItem appearing in + // this list along with manualItem1 and manualItem2 + // (presumably in that order). However, rerunning those + // optimistic transactions would trigger additional + // broadcasts for optimistic query watches, with + // intermediate results that (re)combine optimistic and + // non-optimistic data. Since rerendering the UI tends to be + // expensive, we should prioritize broadcasting states that + // matter most, and in this case that means broadcasting the + // initial optimistic state (for perceived performance), + // followed by the final, authoritative, non-optimistic + // state. Other intermediate states are a distraction, as + // they will probably soon be superseded by another (more + // authoritative) update. This particular state is visible + // only because we haven't rolled back this manual Layer + // just yet (see cache.removeOptimistic below). + manualItem1, + manualItem2, + ], + }, + ROOT_MUTATION: { + __typename: "Mutation", + }, + }); - // Now that the mutation is finished, reading optimistically from - // the cache should return the manually added items again. - expect(cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - items: [ - // If we wanted to keep optimistic data as up-to-date as - // possible, we could rerun all optimistic transactions - // after writing to the root (non-optimistic) layer of the - // cache, which would result in mutationItem appearing in - // this list along with manualItem1 and manualItem2 - // (presumably in that order). However, rerunning those - // optimistic transactions would trigger additional - // broadcasts for optimistic query watches, with - // intermediate results that (re)combine optimistic and - // non-optimistic data. Since rerendering the UI tends to be - // expensive, we should prioritize broadcasting states that - // matter most, and in this case that means broadcasting the - // initial optimistic state (for perceived performance), - // followed by the final, authoritative, non-optimistic - // state. Other intermediate states are a distraction, as - // they will probably soon be superseded by another (more - // authoritative) update. This particular state is visible - // only because we haven't rolled back this manual Layer - // just yet (see cache.removeOptimistic below). - manualItem1, - manualItem2, - ], - }, - ROOT_MUTATION: { - __typename: "Mutation", - }, - }); + cache.removeOptimistic("manual"); - cache.removeOptimistic("manual"); + // After removing the manual optimistic layer, only the + // non-optimistic data remains. + expect(cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + items: [mutationItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + }, + }); - // After removing the manual optimistic layer, only the - // non-optimistic data remains. - expect(cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - items: [mutationItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - }, - }); - }) - .then(() => { - cancelFns.forEach((cancel) => cancel()); + cancelFns.forEach((cancel) => cancel()); - expect(optimisticDiffs).toEqual([ - { - complete: true, - fromOptimisticTransaction: true, - result: { - items: manualItems, - }, - }, - { - complete: true, - fromOptimisticTransaction: true, - result: { - items: [...manualItems, optimisticItem], - }, - }, - { - complete: true, - result: { - items: manualItems, - }, - }, - { - complete: true, - result: { - items: [mutationItem], - }, - }, - ]); + expect(optimisticDiffs).toEqual([ + { + complete: true, + fromOptimisticTransaction: true, + result: { + items: manualItems, + }, + }, + { + complete: true, + fromOptimisticTransaction: true, + result: { + items: [...manualItems, optimisticItem], + }, + }, + { + complete: true, + result: { + items: manualItems, + }, + }, + { + complete: true, + result: { + items: [mutationItem], + }, + }, + ]); - expect(realisticDiffs).toEqual([ - { - complete: false, - missing: [expect.anything()], - result: {}, - }, - { - complete: true, - result: { - items: [mutationItem], - }, - }, - ]); - }) - .then(resolve, reject); + expect(realisticDiffs).toEqual([ + { + complete: false, + missing: [expect.anything()], + result: {}, + }, + { + complete: true, + result: { + items: [mutationItem], + }, + }, + ]); }); }); }); @@ -2403,10 +2203,7 @@ describe("optimistic mutation - githunt comments", () => { }, }; - async function setup( - reject: (reason: any) => any, - ...mockedResponses: any[] - ) { + async function setup(...mockedResponses: MockedResponse[]) { const link = mockSingleLink( { request: { @@ -2423,7 +2220,7 @@ describe("optimistic mutation - githunt comments", () => { result, }, ...mockedResponses - ).setOnError(reject); + ); const client = new ApolloClient({ link, @@ -2501,31 +2298,25 @@ describe("optimistic mutation - githunt comments", () => { }, }; - itAsync("can post a new comment", async (resolve, reject) => { - expect.assertions(1); + it("can post a new comment", async () => { + expect.assertions(3); const mutationVariables = { repoFullName: "org/repo", commentContent: "New Comment", }; - let subscriptionHandle: Subscription; - const client = await setup(reject, { + const client = await setup({ request: { query: addTypenameToDocument(mutation), variables: mutationVariables, }, result: mutationResult, }); + const stream = new ObservableStream( + client.watchQuery({ query, variables }) + ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query, variables }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); await client.mutate({ mutation, @@ -2536,9 +2327,7 @@ describe("optimistic mutation - githunt comments", () => { const newResult: any = await client.query({ query, variables }); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); expect(newResult.data.entry.comments.length).toBe(2); - - resolve(); }); }); diff --git a/src/__tests__/subscribeToMore.ts b/src/__tests__/subscribeToMore.ts index 419b0b696d0..6216eaa85c9 100644 --- a/src/__tests__/subscribeToMore.ts +++ b/src/__tests__/subscribeToMore.ts @@ -4,7 +4,8 @@ import { DocumentNode, OperationDefinitionNode } from "graphql"; import { ApolloClient } from "../core"; import { InMemoryCache } from "../cache"; import { ApolloLink, Operation } from "../link/core"; -import { itAsync, mockSingleLink, mockObservableLink } from "../testing"; +import { mockSingleLink, mockObservableLink, wait } from "../testing"; +import { ObservableStream, spyOnConsole } from "../testing/internal"; const isSub = (operation: Operation) => (operation.query as DocumentNode).definitions @@ -57,13 +58,11 @@ describe("subscribeToMore", () => { name: string; } - itAsync("triggers new result from subscription data", (resolve, reject) => { - let latestResult: any = null; + it("triggers new result from subscription data", async () => { const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req1).setOnError(reject); + const httpLink = mockSingleLink(req1); const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; const client = new ApolloClient({ cache: new InMemoryCache({ addTypename: false }), @@ -71,21 +70,7 @@ describe("subscribeToMore", () => { }); const obsHandle = client.watchQuery({ query }); - - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - if (++counter === 3) { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "Amanda Liu" } }, - loading: false, - networkStatus: 7, - }); - resolve(); - } - }, - }); + const stream = new ObservableStream(obsHandle); obsHandle.subscribeToMore({ document: gql` @@ -98,26 +83,35 @@ describe("subscribeToMore", () => { }, }); - let i = 0; - function simulate() { - const result = results[i++]; - if (result) { - wSLink.simulateResult(result); - setTimeout(simulate, 10); - } - } - simulate(); + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); + + wSLink.simulateResult(results[0]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Dahivat Pandya" } }, + loading: false, + networkStatus: 7, + }); + + await wait(10); + wSLink.simulateResult(results[1]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Amanda Liu" } }, + loading: false, + networkStatus: 7, + }); }); - itAsync("calls error callback on error", (resolve, reject) => { - let latestResult: any = null; + it("calls error callback on error", async () => { const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req1).setOnError(reject); - + const httpLink = mockSingleLink(req1); const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; - const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), @@ -126,14 +120,9 @@ describe("subscribeToMore", () => { const obsHandle = client.watchQuery<(typeof req1)["result"]["data"]>({ query, }); - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - counter++; - }, - }); + const stream = new ObservableStream(obsHandle); - let errorCount = 0; + const onError = jest.fn(); obsHandle.subscribeToMore({ document: gql` @@ -144,98 +133,84 @@ describe("subscribeToMore", () => { updateQuery: (_, { subscriptionData }) => { return { entry: { value: subscriptionData.data.name } }; }, - onError: () => { - errorCount += 1; - }, + onError, }); - setTimeout(() => { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "Amanda Liu" } }, - loading: false, - networkStatus: 7, - }); - expect(counter).toBe(2); - expect(errorCount).toBe(1); - resolve(); - }, 15); - - for (let i = 0; i < 2; i++) { - wSLink.simulateResult(results2[i]); + for (const result of results2) { + wSLink.simulateResult(result); } - }); - itAsync( - "prints unhandled subscription errors to the console", - (resolve, reject) => { - let latestResult: any = null; + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); - const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req1).setOnError(reject); + await expect(stream).toEmitValue({ + data: { entry: { value: "Amanda Liu" } }, + loading: false, + networkStatus: 7, + }); - const link = ApolloLink.split(isSub, wSLink, httpLink); + await wait(15); - let counter = 0; + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith(new Error("You cant touch this")); + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + it("prints unhandled subscription errors to the console", async () => { + using _ = spyOnConsole("error"); - const obsHandle = client.watchQuery({ - query, - }); - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - counter++; - }, - }); + const wSLink = mockObservableLink(); + const httpLink = mockSingleLink(req1); + const link = ApolloLink.split(isSub, wSLink, httpLink); - let errorCount = 0; - const consoleErr = console.error; - console.error = (_: Error) => { - errorCount += 1; - }; + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - obsHandle.subscribeToMore({ - document: gql` - subscription newValues { - name - } - `, - updateQuery: () => { - throw new Error("should not be called because of initial error"); - }, - }); - - setTimeout(() => { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "1" } }, - loading: false, - networkStatus: 7, - }); - expect(counter).toBe(1); - expect(errorCount).toBe(1); - console.error = consoleErr; - resolve(); - }, 15); - - for (let i = 0; i < 2; i++) { - wSLink.simulateResult(results3[i]); - } + const obsHandle = client.watchQuery({ + query, + }); + const stream = new ObservableStream(obsHandle); + + obsHandle.subscribeToMore({ + document: gql` + subscription newValues { + name + } + `, + updateQuery: () => { + throw new Error("should not be called because of initial error"); + }, + }); + + for (const result of results3) { + wSLink.simulateResult(result); } - ); - itAsync("should not corrupt the cache (#3062)", async (resolve, reject) => { - let latestResult: any = null; - const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req4).setOnError(reject); + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); + + await wait(15); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + "Unhandled GraphQL subscription error", + new Error("You cant touch this") + ); + + await expect(stream).not.toEmitAnything(); + }); + it("should not corrupt the cache (#3062)", async () => { + const wSLink = mockObservableLink(); + const httpLink = mockSingleLink(req4); const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; const client = new ApolloClient({ cache: new InMemoryCache({ addTypename: false }).restore({ @@ -256,13 +231,7 @@ describe("subscribeToMore", () => { const obsHandle = client.watchQuery<(typeof req4)["result"]["data"]>({ query, }); - - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - counter++; - }, - }); + const stream = new ObservableStream(obsHandle); let nextMutation: { value: string }; obsHandle.subscribeToMore({ @@ -279,9 +248,6 @@ describe("subscribeToMore", () => { }, }); - const wait = (dur: any) => - new Promise((resolve) => setTimeout(resolve, dur)); - for (let i = 0; i < 2; i++) { // init optimistic mutation let data = client.cache.readQuery<(typeof req4)["result"]["data"]>( @@ -302,105 +268,104 @@ describe("subscribeToMore", () => { client.cache.removeOptimistic(i.toString()); // note: we don't complete mutation with performTransaction because a real example would detect duplicates } - sub.unsubscribe(); - expect(counter).toBe(3); - expect(latestResult).toEqual({ + + await expect(stream).toEmitValue({ + data: { entry: [{ value: 1 }, { value: 2 }] }, + loading: false, + networkStatus: 7, + }); + + await expect(stream).toEmitValue({ + data: { + entry: [{ value: 1 }, { value: 2 }, { value: "Dahivat Pandya" }], + }, + loading: false, + networkStatus: 7, + }); + + await expect(stream).toEmitValue({ data: { entry: [ - { - value: 1, - }, - { - value: 2, - }, - { - value: "Dahivat Pandya", - }, - { - value: "Amanda Liu", - }, + { value: 1 }, + { value: 2 }, + { value: "Dahivat Pandya" }, + { value: "Amanda Liu" }, ], }, loading: false, networkStatus: 7, }); - resolve(); + + await expect(stream).not.toEmitAnything(); }); // TODO add a test that checks that subscriptions are cancelled when obs is unsubscribed from. - itAsync( - "allows specification of custom types for variables and payload (#4246)", - (resolve, reject) => { - interface TypedOperation extends Operation { - variables: { - someNumber: number; - }; - } - const typedReq = { - request: { query, variables: { someNumber: 1 } } as TypedOperation, - result, + it("allows specification of custom types for variables and payload (#4246)", async () => { + interface TypedOperation extends Operation { + variables: { + someNumber: number; }; - interface TypedSubscriptionVariables { - someString: string; - } + } + const typedReq = { + request: { query, variables: { someNumber: 1 } } as TypedOperation, + result, + }; + interface TypedSubscriptionVariables { + someString: string; + } - let latestResult: any = null; - const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(typedReq).setOnError(reject); - - const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; - - const client = new ApolloClient({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); - - type TData = (typeof typedReq)["result"]["data"]; - type TVars = (typeof typedReq)["request"]["variables"]; - const obsHandle = client.watchQuery({ - query, - variables: { someNumber: 1 }, - }); - - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - if (++counter === 3) { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "Amanda Liu" } }, - loading: false, - networkStatus: 7, - }); - resolve(); - } - }, - }); - - obsHandle.subscribeToMore({ - document: gql` - subscription newValues { - name - } - `, - variables: { - someString: "foo", - }, - updateQuery: (_, { subscriptionData }) => { - return { entry: { value: subscriptionData.data.name } }; - }, - }); - - let i = 0; - function simulate() { - const result = results[i++]; - if (result) { - wSLink.simulateResult(result); - setTimeout(simulate, 10); + const wSLink = mockObservableLink(); + const httpLink = mockSingleLink(typedReq); + const link = ApolloLink.split(isSub, wSLink, httpLink); + + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link, + }); + + type TData = (typeof typedReq)["result"]["data"]; + type TVars = (typeof typedReq)["request"]["variables"]; + const obsHandle = client.watchQuery({ + query, + variables: { someNumber: 1 }, + }); + const stream = new ObservableStream(obsHandle); + + obsHandle.subscribeToMore({ + document: gql` + subscription newValues { + name } - } - simulate(); - } - ); + `, + variables: { + someString: "foo", + }, + updateQuery: (_, { subscriptionData }) => { + return { entry: { value: subscriptionData.data.name } }; + }, + }); + + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); + + wSLink.simulateResult(results[0]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Dahivat Pandya" } }, + loading: false, + networkStatus: 7, + }); + + await wait(10); + wSLink.simulateResult(results[1]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Amanda Liu" } }, + loading: false, + networkStatus: 7, + }); + }); }); diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index 1edd4e2c2f1..32bf53b64ee 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -4,7 +4,7 @@ import { map } from "rxjs/operators"; import { assign } from "lodash"; import gql from "graphql-tag"; import { DocumentNode, GraphQLError } from "graphql"; -import { setVerbosity } from "ts-invariant"; +import { InvariantError, setVerbosity } from "ts-invariant"; import { Observable, @@ -28,32 +28,22 @@ import { } from "../../../testing/core/mocking/mockLink"; // core -import { ApolloQueryResult } from "../../types"; import { NetworkStatus } from "../../networkStatus"; import { ObservableQuery } from "../../ObservableQuery"; -import { - MutationBaseOptions, - MutationOptions, - WatchQueryOptions, -} from "../../watchQueryOptions"; +import { WatchQueryOptions } from "../../watchQueryOptions"; import { QueryManager } from "../../QueryManager"; import { ApolloError } from "../../../errors"; // testing utils import { waitFor } from "@testing-library/react"; -import wrap from "../../../testing/core/wrap"; -import observableToPromise, { - observableToPromiseAndSubscription, -} from "../../../testing/core/observableToPromise"; -import { itAsync } from "../../../testing/core"; +import { wait } from "../../../testing/core"; import { ApolloClient } from "../../../core"; import { mockFetchQuery } from "../ObservableQuery"; import { Concast, print } from "../../../utilities"; -import { ObservableStream } from "../../../testing/internal"; +import { ObservableStream, spyOnConsole } from "../../../testing/internal"; interface MockedMutation { - reject: (reason: any) => any; mutation: DocumentNode; data?: Object; errors?: GraphQLError[]; @@ -106,24 +96,20 @@ describe("QueryManager", () => { // Helper method that sets up a mockQueryManager and then passes on the // results to an observer. - const assertWithObserver = ({ - reject, + const getObservableStream = ({ query, variables = {}, queryOptions = {}, result, error, delay, - observer, }: { - reject: (reason: any) => any; query: DocumentNode; variables?: Object; queryOptions?: Object; error?: Error; result?: FetchResult; delay?: number; - observer: Observer>; }) => { const queryManager = mockQueryManager({ request: { query, variables }, @@ -131,18 +117,13 @@ describe("QueryManager", () => { error, delay, }); - const finalOptions = assign( - { query, variables }, - queryOptions - ) as WatchQueryOptions; - return queryManager.watchQuery(finalOptions).subscribe({ - next: wrap(reject, observer.next!), - error: observer.error, - }); + + return new ObservableStream( + queryManager.watchQuery(assign({ query, variables }, queryOptions)) + ); }; const mockMutation = ({ - reject, mutation, data, errors, @@ -152,7 +133,7 @@ describe("QueryManager", () => { const link = mockSingleLink({ request: { query: mutation, variables }, result: { data, errors }, - }).setOnError(reject); + }); const queryManager = createQueryManager({ link, @@ -174,18 +155,6 @@ describe("QueryManager", () => { }); }; - const assertMutationRoundtrip = ( - resolve: (result: any) => any, - opts: MockedMutation - ) => { - const { reject } = opts; - return mockMutation(opts) - .then(({ result }) => { - expect(result.data).toEqual(opts.data); - }) - .then(resolve, reject); - }; - // Helper method that takes a query with a first response and a second response. // Used to assert stuff about refetches. const mockRefetch = ({ @@ -230,9 +199,8 @@ describe("QueryManager", () => { }; } - itAsync("handles GraphQL errors", (resolve, reject) => { - assertWithObserver({ - reject, + it("handles GraphQL errors", async () => { + const stream = getObservableStream({ query: gql` query people { allPeople(first: 1) { @@ -246,24 +214,17 @@ describe("QueryManager", () => { result: { errors: [new GraphQLError("This is an error message.")], }, - observer: { - next() { - reject( - new Error("Returned a result when it was supposed to error out") - ); - }, - - error(apolloError) { - expect(apolloError).toBeDefined(); - resolve(); - }, - }, }); + + await expect(stream).toEmitError( + new ApolloError({ + graphQLErrors: [{ message: "This is an error message." }], + }) + ); }); - itAsync("handles GraphQL errors as data", (resolve, reject) => { - assertWithObserver({ - reject, + it("handles GraphQL errors as data", async () => { + const stream = getObservableStream({ query: gql` query people { allPeople(first: 1) { @@ -280,26 +241,18 @@ describe("QueryManager", () => { result: { errors: [new GraphQLError("This is an error message.")], }, - observer: { - next({ errors }) { - expect(errors).toBeDefined(); - expect(errors![0].message).toBe("This is an error message."); - resolve(); - }, - error(apolloError) { - reject( - new Error( - "Called observer.error instead of passing errors to observer.next" - ) - ); - }, - }, + }); + + await expect(stream).toEmitValue({ + data: undefined, + loading: false, + networkStatus: 8, + errors: [{ message: "This is an error message." }], }); }); - itAsync("handles GraphQL errors with data returned", (resolve, reject) => { - assertWithObserver({ - reject, + it("handles GraphQL errors with data returned", async () => { + const stream = getObservableStream({ query: gql` query people { allPeople(first: 1) { @@ -319,90 +272,56 @@ describe("QueryManager", () => { }, errors: [new GraphQLError("This is an error message.")], }, - observer: { - next() { - reject(new Error("Returned data when it was supposed to error out.")); - }, - - error(apolloError) { - expect(apolloError).toBeDefined(); - resolve(); - }, - }, }); + + await expect(stream).toEmitError( + new ApolloError({ + graphQLErrors: [{ message: "This is an error message." }], + }) + ); }); - itAsync( - "empty error array (handle non-spec-compliant server) #156", - (resolve, reject) => { - assertWithObserver({ - reject, - query: gql` - query people { - allPeople(first: 1) { - people { - name - } + it("empty error array (handle non-spec-compliant server) #156", async () => { + const stream = getObservableStream({ + query: gql` + query people { + allPeople(first: 1) { + people { + name } } - `, - result: { - data: { - allPeople: { - people: { - name: "Ada Lovelace", - }, + } + `, + result: { + data: { + allPeople: { + people: { + name: "Ada Lovelace", }, }, - errors: [], }, - observer: { - next(result) { - expect(result.data["allPeople"].people.name).toBe("Ada Lovelace"); - expect(result["errors"]).toBeUndefined(); - resolve(); + errors: [], + }, + }); + + await expect(stream).toEmitValue({ + errors: undefined, + data: { + allPeople: { + people: { + name: "Ada Lovelace", }, }, - }); - } - ); + }, + networkStatus: 7, + loading: false, + }); + }); // Easy to get into this state if you write an incorrect `formatError` // function with graphql-server or express-graphql - itAsync( - "error array with nulls (handle non-spec-compliant server) #1185", - (resolve, reject) => { - assertWithObserver({ - reject, - query: gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `, - result: { - errors: [null as any], - }, - observer: { - next() { - reject(new Error("Should not fire next for an error")); - }, - error(error) { - expect((error as any).graphQLErrors).toEqual([null]); - expect(error.message).toBe("Error message not found."); - resolve(); - }, - }, - }); - } - ); - - itAsync("handles network errors", (resolve, reject) => { - assertWithObserver({ - reject, + it("error array with nulls (handle non-spec-compliant server) #1185", async () => { + const stream = getObservableStream({ query: gql` query people { allPeople(first: 1) { @@ -412,30 +331,20 @@ describe("QueryManager", () => { } } `, - error: new Error("Network error"), - observer: { - next: () => { - reject(new Error("Should not deliver result")); - }, - error: (error) => { - const apolloError = error as ApolloError; - expect(apolloError.networkError).toBeDefined(); - expect(apolloError.networkError!.message).toMatch("Network error"); - resolve(); - }, + result: { + errors: [null as any], }, }); - }); - itAsync("uses console.error to log unhandled errors", (resolve, reject) => { - const oldError = console.error; - let printed: any; - console.error = (...args: any[]) => { - printed = args; - }; + await expect(stream).toEmitError( + new ApolloError({ + graphQLErrors: [null as any], + }) + ); + }); - assertWithObserver({ - reject, + it("handles network errors", async () => { + const stream = getObservableStream({ query: gql` query people { allPeople(first: 1) { @@ -446,53 +355,71 @@ describe("QueryManager", () => { } `, error: new Error("Network error"), - observer: { - next: () => { - reject(new Error("Should not deliver result")); - }, + }); + + await expect(stream).toEmitError( + new ApolloError({ + networkError: new Error("Network error"), + }) + ); + }); + + it("uses console.error to log unhandled errors", async () => { + using _ = spyOnConsole("error"); + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; + const error = new Error("Network error"); + + const queryManager = mockQueryManager({ + request: { query }, + error, + }); + + const observable = queryManager.watchQuery({ query }); + observable.subscribe({ + next: () => { + throw new Error("Should not have been called"); }, }); - setTimeout(() => { - expect(printed[0]).toMatch(/error/); - console.error = oldError; - resolve(); - }, 10); + await wait(10); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + "Unhandled error", + "Network error", + expect.anything() + ); }); // XXX this looks like a bug in zen-observable but we should figure // out a solution for it - itAsync.skip( - "handles an unsubscribe action that happens before data returns", - (resolve, reject) => { - const subscription = assertWithObserver({ - reject, - query: gql` - query people { - allPeople(first: 1) { - people { - name - } + it.skip("handles an unsubscribe action that happens before data returns", async () => { + const stream = getObservableStream({ + query: gql` + query people { + allPeople(first: 1) { + people { + name } } - `, - delay: 1000, - observer: { - next: () => { - reject(new Error("Should not deliver result")); - }, - error: () => { - reject(new Error("Should not deliver result")); - }, - }, - }); + } + `, + delay: 1000, + }); - expect(subscription.unsubscribe).not.toThrow(); - } - ); + expect(stream.unsubscribe).not.toThrow(); + }); // Query should be aborted on last .unsubscribe() - itAsync("causes link unsubscription if unsubscribed", (resolve, reject) => { + it("causes link unsubscription if unsubscribed", async () => { const expResult = { data: { allPeople: { @@ -557,25 +484,17 @@ describe("QueryManager", () => { notifyOnNetworkStatusChange: false, }); - const observerCallback = wrap(reject, () => { - reject(new Error("Link subscription should have been cancelled")); - }); + const stream = new ObservableStream(observableQuery); - const subscription = observableQuery.subscribe({ - next: observerCallback, - error: observerCallback, - complete: observerCallback, - }); + stream.unsubscribe(); - subscription.unsubscribe(); + await wait(10); - return waitFor(() => { - expect(onRequestUnsubscribe).toHaveBeenCalledTimes(1); - expect(onRequestSubscribe).toHaveBeenCalledTimes(1); - }).then(resolve, reject); + expect(onRequestUnsubscribe).toHaveBeenCalledTimes(1); + expect(onRequestSubscribe).toHaveBeenCalledTimes(1); }); - itAsync("causes link unsubscription after reobserve", (resolve, reject) => { + it("causes link unsubscription after reobserve", async () => { const expResult = { data: { allPeople: { @@ -654,86 +573,72 @@ describe("QueryManager", () => { variables: request.variables, }); - const observerCallback = wrap(reject, () => { - reject(new Error("Link subscription should have been cancelled")); - }); - - const subscription = observableQuery.subscribe({ - next: observerCallback, - error: observerCallback, - complete: observerCallback, - }); + const stream = new ObservableStream(observableQuery); expect(onRequestSubscribe).toHaveBeenCalledTimes(1); // This is the most important part of this test // Check that reobserve cancels the previous connection while watchQuery remains active - observableQuery.reobserve({ variables: { offset: 20 } }); + void observableQuery.reobserve({ variables: { offset: 20 } }); - return waitFor(() => { + await waitFor(() => { // Verify that previous connection was aborted by reobserve expect(onRequestUnsubscribe).toHaveBeenCalledTimes(1); - }) - .then(async () => { - subscription.unsubscribe(); - await waitFor(() => { - expect(onRequestSubscribe).toHaveBeenCalledTimes(2); - }); - await waitFor(() => { - expect(onRequestUnsubscribe).toHaveBeenCalledTimes(2); - }); - }) - .then(resolve, reject); + }); + + stream.unsubscribe(); + + await wait(10); + + expect(onRequestSubscribe).toHaveBeenCalledTimes(2); + expect(onRequestUnsubscribe).toHaveBeenCalledTimes(2); }); - itAsync( - "supports interoperability with other Observable implementations like RxJS", - (resolve, reject) => { - const expResult = { - data: { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, + it("supports interoperability with other Observable implementations like RxJS", async () => { + const expResult = { + data: { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], }, - }; + }, + }; - const handle = mockWatchQuery({ - request: { - query: gql` - query people { - allPeople(first: 1) { - people { - name - } + const handle = mockWatchQuery({ + request: { + query: gql` + query people { + allPeople(first: 1) { + people { + name } } - `, - }, - result: expResult, - }); + } + `, + }, + result: expResult, + }); - const observable = from(handle as any); + const observable = from(handle as any); - observable - .pipe(map((result) => assign({ fromRx: true }, result))) - .subscribe({ - next: wrap(reject, (newResult) => { - const expectedResult = assign( - { fromRx: true, loading: false, networkStatus: 7 }, - expResult - ); - expect(newResult).toEqual(expectedResult); - resolve(); - }), - }); - } - ); + const stream = new ObservableStream( + observable.pipe( + map((result) => assign({ fromRx: true }, result)) + ) as unknown as Observable + ); + + await expect(stream).toEmitValue({ + fromRx: true, + loading: false, + networkStatus: 7, + ...expResult, + }); + }); - itAsync("allows you to subscribe twice to one query", (resolve, reject) => { + it("allows you to subscribe twice to one query", async () => { const request = { query: gql` query fetchLuke($id: String) { @@ -782,59 +687,29 @@ describe("QueryManager", () => { } ); - let subOneCount = 0; - // pre populate data to avoid contention - queryManager.query(request).then(() => { - const handle = queryManager.watchQuery(request); + await queryManager.query(request); - const subOne = handle.subscribe({ - next(result) { - subOneCount++; + const handle = queryManager.watchQuery(request); - if (subOneCount === 1) { - expect(result.data).toEqual(data1); - } else if (subOneCount === 2) { - expect(result.data).toEqual(data2); - } - }, - }); + const stream1 = new ObservableStream(handle); + const stream2 = new ObservableStream(handle); - let subTwoCount = 0; - handle.subscribe({ - next(result) { - subTwoCount++; - if (subTwoCount === 1) { - expect(result.data).toEqual(data1); - handle.refetch(); - } else if (subTwoCount === 2) { - expect(result.data).toEqual(data2); - setTimeout(() => { - try { - expect(subOneCount).toBe(2); - - subOne.unsubscribe(); - handle.refetch(); - } catch (e) { - reject(e); - } - }, 0); - } else if (subTwoCount === 3) { - setTimeout(() => { - try { - expect(subOneCount).toBe(2); - resolve(); - } catch (e) { - reject(e); - } - }, 0); - } - }, - }); - }); + await expect(stream1).toEmitMatchedValue({ data: data1 }); + await expect(stream2).toEmitMatchedValue({ data: data1 }); + + void handle.refetch(); + + await expect(stream1).toEmitMatchedValue({ data: data2 }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); + + stream1.unsubscribe(); + void handle.refetch(); + + await expect(stream2).toEmitMatchedValue({ data: data3 }); }); - itAsync("resolves all queries when one finishes after another", (resolve) => { + it("resolves all queries when one finishes after another", async () => { const request = { query: gql` query fetchLuke($id: String) { @@ -914,23 +789,16 @@ describe("QueryManager", () => { const ob2 = queryManager.watchQuery(request2); const ob3 = queryManager.watchQuery(request3); - let finishCount = 0; - ob1.subscribe((result) => { - expect(result.data).toEqual(data1); - finishCount++; - }); - ob2.subscribe((result) => { - expect(result.data).toEqual(data2); - expect(finishCount).toBe(2); - resolve(); - }); - ob3.subscribe((result) => { - expect(result.data).toEqual(data3); - finishCount++; - }); + const stream1 = new ObservableStream(ob1); + const stream2 = new ObservableStream(ob2); + const stream3 = new ObservableStream(ob3); + + await expect(stream1).toEmitMatchedValue({ data: data1 }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); + await expect(stream3).toEmitMatchedValue({ data: data3 }); }); - itAsync("allows you to refetch queries", (resolve, reject) => { + it("allows you to refetch queries", async () => { const request = { query: gql` query fetchLuke($id: String) { @@ -963,336 +831,220 @@ describe("QueryManager", () => { }); const observable = queryManager.watchQuery(request); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => expect(result.data).toEqual(data2) - ).then(resolve, reject); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data: data1 }); + void observable.refetch(); + await expect(stream).toEmitMatchedValue({ data: data2 }); }); - itAsync( - "will return referentially equivalent data if nothing changed in a refetch", - (resolve, reject) => { - const request: WatchQueryOptions = { - query: gql` - { - a - b { - c - } - d { - e - f { - g - } + it("will return referentially equivalent data if nothing changed in a refetch", async () => { + const request: WatchQueryOptions = { + query: gql` + { + a + b { + c + } + d { + e + f { + g } } - `, - notifyOnNetworkStatusChange: false, - canonizeResults: true, - }; + } + `, + notifyOnNetworkStatusChange: false, + canonizeResults: true, + }; - const data1 = { - a: 1, - b: { c: 2 }, - d: { e: 3, f: { g: 4 } }, - }; + const data1 = { + a: 1, + b: { c: 2 }, + d: { e: 3, f: { g: 4 } }, + }; - const data2 = { - a: 1, - b: { c: 2 }, - d: { e: 30, f: { g: 4 } }, - }; + const data2 = { + a: 1, + b: { c: 2 }, + d: { e: 30, f: { g: 4 } }, + }; - const data3 = { - a: 1, - b: { c: 2 }, - d: { e: 3, f: { g: 4 } }, - }; + const data3 = { + a: 1, + b: { c: 2 }, + d: { e: 3, f: { g: 4 } }, + }; - const queryManager = mockRefetch({ - request, - firstResult: { data: data1 }, - secondResult: { data: data2 }, - thirdResult: { data: data3 }, - }); + const queryManager = mockRefetch({ + request, + firstResult: { data: data1 }, + secondResult: { data: data2 }, + thirdResult: { data: data3 }, + }); - const observable = queryManager.watchQuery(request); + const observable = queryManager.watchQuery(request); + const stream = new ObservableStream(observable); - let count = 0; - let firstResultData: any; + const { data: firstResultData } = await stream.takeNext(); + expect(firstResultData).toEqual(data1); - observable.subscribe({ - next: (result) => { - try { - switch (count++) { - case 0: - expect(result.data).toEqual(data1); - firstResultData = result.data; - observable.refetch(); - break; - case 1: - expect(result.data).toEqual(data2); - expect(result.data).not.toEqual(firstResultData); - expect(result.data.b).toEqual(firstResultData.b); - expect(result.data.d).not.toEqual(firstResultData.d); - expect(result.data.d.f).toEqual(firstResultData.d.f); - observable.refetch(); - break; - case 2: - expect(result.data).toEqual(data3); - expect(result.data).toBe(firstResultData); - resolve(); - break; - default: - throw new Error("Next run too many times."); - } - } catch (error) { - reject(error); - } - }, - error: reject, - }); - } - ); + void observable.refetch(); - itAsync( - "will return referentially equivalent data in getCurrentResult if nothing changed", - (resolve, reject) => { - const request = { - query: gql` - { - a - b { - c - } - d { - e - f { - g - } - } - } - `, - notifyOnNetworkStatusChange: false, - }; + { + const result = await stream.takeNext(); - const data1 = { - a: 1, - b: { c: 2 }, - d: { e: 3, f: { g: 4 } }, - }; + expect(result.data).toEqual(data2); + expect(result.data).not.toEqual(firstResultData); + expect(result.data.b).toEqual(firstResultData.b); + expect(result.data.d).not.toEqual(firstResultData.d); + expect(result.data.d.f).toEqual(firstResultData.d.f); + } - const queryManager = mockQueryManager({ - request, - result: { data: data1 }, - }); + void observable.refetch(); - const observable = queryManager.watchQuery(request); + { + const result = await stream.takeNext(); - observable.subscribe({ - next: (result) => { - try { - expect(result.data).toEqual(data1); - expect(result.data).toEqual(observable.getCurrentResult().data); - resolve(); - } catch (error) { - reject(error); - } - }, - error: reject, - }); + expect(result.data).toEqual(data3); + expect(result.data).toBe(firstResultData); } - ); + }); - itAsync( - "sets networkStatus to `refetch` when refetching", - (resolve, reject) => { - const request: WatchQueryOptions = { - query: gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name + it("will return referentially equivalent data in getCurrentResult if nothing changed", async () => { + const request = { + query: gql` + { + a + b { + c + } + d { + e + f { + g } } - `, - variables: { - id: "1", - }, - notifyOnNetworkStatusChange: true, - // This causes a loading:true result to be delivered from the cache - // before the final data2 result is delivered. - fetchPolicy: "cache-and-network", - }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; - - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; - - const queryManager = mockRefetch({ - request, - firstResult: { data: data1 }, - secondResult: { data: data2 }, - }); - - const observable = queryManager.watchQuery(request); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => expect(result.networkStatus).toBe(NetworkStatus.refetch), - (result) => { - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(result.data).toEqual(data2); } - ).then(resolve, reject); - } - ); + `, + notifyOnNetworkStatusChange: false, + }; - itAsync( - "allows you to refetch queries with promises", - async (resolve, reject) => { - const request = { - query: gql` - { - people_one(id: 1) { - name - } - } - `, - }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data1 = { + a: 1, + b: { c: 2 }, + d: { e: 3, f: { g: 4 } }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const queryManager = mockQueryManager({ + request, + result: { data: data1 }, + }); - const queryManager = mockRefetch({ - request, - firstResult: { data: data1 }, - secondResult: { data: data2 }, - }); + const observable = queryManager.watchQuery(request); + const stream = new ObservableStream(observable); - const handle = queryManager.watchQuery(request); - handle.subscribe({}); + const { data } = await stream.takeNext(); - return handle - .refetch() - .then((result) => expect(result.data).toEqual(data2)) - .then(resolve, reject); - } - ); + expect(data).toEqual(data1); + expect(data).toBe(observable.getCurrentResult().data); + }); - itAsync( - "allows you to refetch queries with new variables", - (resolve, reject) => { - const query = gql` - { - people_one(id: 1) { + it("sets networkStatus to `refetch` when refetching", async () => { + const request: WatchQueryOptions = { + query: gql` + query fetchLuke($id: String) { + people_one(id: $id) { name } } - `; + `, + variables: { + id: "1", + }, + notifyOnNetworkStatusChange: true, + // This causes a loading:true result to be delivered from the cache + // before the final data2 result is delivered. + fetchPolicy: "cache-and-network", + }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const queryManager = mockRefetch({ + request, + firstResult: { data: data1 }, + secondResult: { data: data2 }, + }); - const data3 = { - people_one: { - name: "Luke Skywalker has a new name and age", - }, - }; + const observable = queryManager.watchQuery(request); + const stream = new ObservableStream(observable); - const data4 = { - people_one: { - name: "Luke Skywalker has a whole new bag", - }, - }; + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); - const variables1 = { - test: "I am your father", - }; + void observable.refetch(); - const variables2 = { - test: "No. No! That's not true! That's impossible!", - }; + await expect(stream).toEmitValue({ + data: data1, + loading: true, + networkStatus: NetworkStatus.refetch, + }); + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); - const queryManager = mockQueryManager( - { - request: { query: query }, - result: { data: data1 }, - }, - { - request: { query: query }, - result: { data: data2 }, - }, - { - request: { query: query, variables: variables1 }, - result: { data: data3 }, - }, + it("allows you to refetch queries with promises", async () => { + const request = { + query: gql` { - request: { query: query, variables: variables2 }, - result: { data: data4 }, + people_one(id: 1) { + name + } } - ); + `, + }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const observable = queryManager.watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }); - return observableToPromise( - { observable }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data1); - return observable.refetch(); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data2); - return observable.refetch(variables1); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data3); - return observable.refetch(variables2); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data4); - } - ).then(resolve, reject); - } - ); + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; + + const queryManager = mockRefetch({ + request, + firstResult: { data: data1 }, + secondResult: { data: data2 }, + }); + + const handle = queryManager.watchQuery(request); + handle.subscribe({}); - itAsync("only modifies varaibles when refetching", (resolve, reject) => { + const result = await handle.refetch(); + + expect(result.data).toEqual(data2); + }); + + it("allows you to refetch queries with new variables", async () => { const query = gql` { people_one(id: 1) { @@ -1313,6 +1065,26 @@ describe("QueryManager", () => { }, }; + const data3 = { + people_one: { + name: "Luke Skywalker has a new name and age", + }, + }; + + const data4 = { + people_one: { + name: "Luke Skywalker has a whole new bag", + }, + }; + + const variables1 = { + test: "I am your father", + }; + + const variables2 = { + test: "No. No! That's not true! That's impossible!", + }; + const queryManager = mockQueryManager( { request: { query: query }, @@ -1321,6 +1093,14 @@ describe("QueryManager", () => { { request: { query: query }, result: { data: data2 }, + }, + { + request: { query: query, variables: variables1 }, + result: { data: data3 }, + }, + { + request: { query: query, variables: variables2 }, + result: { data: data4 }, } ); @@ -1328,24 +1108,40 @@ describe("QueryManager", () => { query, notifyOnNetworkStatusChange: false, }); - const originalOptions = assign({}, observable.options); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => { - expect(result.data).toEqual(data2); - const updatedOptions = assign({}, observable.options); - delete originalOptions.variables; - delete updatedOptions.variables; - expect(updatedOptions).toEqual(originalOptions); - } - ).then(resolve, reject); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(); + + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(variables1); + + await expect(stream).toEmitValue({ + data: data3, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(variables2); + + await expect(stream).toEmitValue({ + data: data4, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); - itAsync("continues to poll after refetch", (resolve, reject) => { + it("only modifies varaibles when refetching", async () => { const query = gql` { people_one(id: 1) { @@ -1366,115 +1162,184 @@ describe("QueryManager", () => { }, }; - const data3 = { - people_one: { - name: "Patsy", - }, - }; - const queryManager = mockQueryManager( { - request: { query }, + request: { query: query }, result: { data: data1 }, }, { - request: { query }, + request: { query: query }, result: { data: data2 }, - }, - { - request: { query }, - result: { data: data3 }, } ); const observable = queryManager.watchQuery({ query, - pollInterval: 200, notifyOnNetworkStatusChange: false, }); + const stream = new ObservableStream(observable); + const originalOptions = assign({}, observable.options); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => expect(result.data).toEqual(data2), - (result) => { - expect(result.data).toEqual(data3); - observable.stopPolling(); - } - ).then(resolve, reject); - }); + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); - itAsync( - "sets networkStatus to `poll` if a polling query is in flight", - (resolve) => { - const query = gql` - { - people_one(id: 1) { - name - } + void observable.refetch(); + + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + const updatedOptions = assign({}, observable.options); + delete originalOptions.variables; + delete updatedOptions.variables; + expect(updatedOptions).toEqual(originalOptions); + }); + + it("continues to poll after refetch", async () => { + const query = gql` + { + people_one(id: 1) { + name } - `; + } + `; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; - const data3 = { - people_one: { - name: "Patsy", - }, - }; + const data3 = { + people_one: { + name: "Patsy", + }, + }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data: data1 }, - }, - { - request: { query }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: data3 }, + const queryManager = mockQueryManager( + { + request: { query }, + result: { data: data1 }, + }, + { + request: { query }, + result: { data: data2 }, + }, + { + request: { query }, + result: { data: data3 }, + } + ); + + const observable = queryManager.watchQuery({ + query, + pollInterval: 200, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(); + + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitValue( + { + data: data3, + loading: false, + networkStatus: NetworkStatus.ready, + }, + { timeout: 250 } + ); + + observable.stopPolling(); + }); + + it("sets networkStatus to `poll` if a polling query is in flight", async () => { + const query = gql` + { + people_one(id: 1) { + name } - ); + } + `; - const observable = queryManager.watchQuery({ - query, - pollInterval: 30, - notifyOnNetworkStatusChange: true, - }); + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - let counter = 0; - const handle = observable.subscribe({ - next(result) { - counter += 1; - - if (counter === 1) { - expect(result.networkStatus).toBe(NetworkStatus.ready); - } else if (counter === 2) { - expect(result.networkStatus).toBe(NetworkStatus.poll); - handle.unsubscribe(); - resolve(); - } - }, - }); - } - ); + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; + + const data3 = { + people_one: { + name: "Patsy", + }, + }; + + const queryManager = mockQueryManager( + { + request: { query }, + result: { data: data1 }, + }, + { + request: { query }, + result: { data: data2 }, + }, + { + request: { query }, + result: { data: data3 }, + } + ); + + const observable = queryManager.watchQuery({ + query, + pollInterval: 30, + notifyOnNetworkStatusChange: true, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitValue({ + data: data1, + loading: true, + networkStatus: NetworkStatus.poll, + }); + + stream.unsubscribe(); + }); - itAsync("can handle null values in arrays (#1551)", (resolve) => { + it("can handle null values in arrays (#1551)", async () => { const query = gql` { list { @@ -1488,72 +1353,61 @@ describe("QueryManager", () => { result: { data }, }); const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - observable.subscribe({ - next: (result) => { - expect(result.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - resolve(); - }, - }); + await expect(stream).toEmitMatchedValue({ data }); + expect(observable.getCurrentResult().data).toEqual(data); }); - itAsync( - "supports cache-only fetchPolicy fetching only cached data", - (resolve, reject) => { - const primeQuery = gql` - query primeQuery { - luke: people_one(id: 1) { - name - } + it("supports cache-only fetchPolicy fetching only cached data", async () => { + const primeQuery = gql` + query primeQuery { + luke: people_one(id: 1) { + name } - `; + } + `; - const complexQuery = gql` - query complexQuery { - luke: people_one(id: 1) { - name - } - vader: people_one(id: 4) { - name - } + const complexQuery = gql` + query complexQuery { + luke: people_one(id: 1) { + name } - `; + vader: people_one(id: 4) { + name + } + } + `; - const data1 = { - luke: { - name: "Luke Skywalker", - }, - }; + const data1 = { + luke: { + name: "Luke Skywalker", + }, + }; - const queryManager = mockQueryManager({ - request: { query: primeQuery }, - result: { data: data1 }, - }); + const queryManager = mockQueryManager({ + request: { query: primeQuery }, + result: { data: data1 }, + }); - // First, prime the cache - return queryManager - .query({ - query: primeQuery, - }) - .then(() => { - const handle = queryManager.watchQuery({ - query: complexQuery, - fetchPolicy: "cache-only", - }); + // First, prime the cache + await queryManager.query({ + query: primeQuery, + }); - return handle.result().then((result) => { - expect(result.data["luke"].name).toBe("Luke Skywalker"); - expect(result.data).not.toHaveProperty("vader"); - }); - }) - .then(resolve, reject); - } - ); + const handle = queryManager.watchQuery({ + query: complexQuery, + fetchPolicy: "cache-only", + }); + + const result = await handle.result(); + + expect(result.data["luke"].name).toBe("Luke Skywalker"); + expect(result.data).not.toHaveProperty("vader"); + }); - itAsync("runs a mutation", (resolve, reject) => { - return assertMutationRoundtrip(resolve, { - reject, + it("runs a mutation", async () => { + const { result } = await mockMutation({ mutation: gql` mutation makeListPrivate { makeListPrivate(id: "5") @@ -1561,31 +1415,29 @@ describe("QueryManager", () => { `, data: { makeListPrivate: true }, }); + + expect(result.data).toEqual({ makeListPrivate: true }); }); - itAsync( - "runs a mutation even when errors is empty array #2912", - (resolve, reject) => { - return assertMutationRoundtrip(resolve, { - reject, - mutation: gql` - mutation makeListPrivate { - makeListPrivate(id: "5") - } - `, - errors: [], - data: { makeListPrivate: true }, - }); - } - ); + it("runs a mutation even when errors is empty array #2912", async () => { + const { result } = await mockMutation({ + mutation: gql` + mutation makeListPrivate { + makeListPrivate(id: "5") + } + `, + errors: [], + data: { makeListPrivate: true }, + }); + + expect(result.data).toEqual({ makeListPrivate: true }); + }); - itAsync( - 'runs a mutation with default errorPolicy equal to "none"', - (resolve, reject) => { - const errors = [new GraphQLError("foo")]; + it('runs a mutation with default errorPolicy equal to "none"', async () => { + const errors = [new GraphQLError("foo")]; - return mockMutation({ - reject, + await expect( + mockMutation({ mutation: gql` mutation makeListPrivate { makeListPrivate(id: "5") @@ -1593,23 +1445,15 @@ describe("QueryManager", () => { `, errors, }) - .then( - (result) => { - throw new Error( - "Mutation should not be successful with default errorPolicy" - ); - }, - (error) => { - expect(error.graphQLErrors).toEqual(errors); - } - ) - .then(resolve, reject); - } - ); + ).rejects.toThrow( + expect.objectContaining({ + graphQLErrors: errors, + }) + ); + }); - itAsync("runs a mutation with variables", (resolve, reject) => { - return assertMutationRoundtrip(resolve, { - reject, + it("runs a mutation with variables", async () => { + const { result } = await mockMutation({ mutation: gql` mutation makeListPrivate($listId: ID!) { makeListPrivate(id: $listId) @@ -1618,126 +1462,108 @@ describe("QueryManager", () => { variables: { listId: "1" }, data: { makeListPrivate: true }, }); + + expect(result.data).toEqual({ makeListPrivate: true }); }); const getIdField = (obj: any) => obj.id; - itAsync( - "runs a mutation with object parameters and puts the result in the store", - (resolve, reject) => { - const data = { - makeListPrivate: { - id: "5", - isPrivate: true, - }, - }; - return mockMutation({ - reject, - mutation: gql` - mutation makeListPrivate { - makeListPrivate(input: { id: "5" }) { - id - isPrivate - } + it("runs a mutation with object parameters and puts the result in the store", async () => { + const data = { + makeListPrivate: { + id: "5", + isPrivate: true, + }, + }; + const { result, queryManager } = await mockMutation({ + mutation: gql` + mutation makeListPrivate { + makeListPrivate(input: { id: "5" }) { + id + isPrivate } - `, - data, - config: { dataIdFromObject: getIdField }, - }) - .then(({ result, queryManager }) => { - expect(result.data).toEqual(data); + } + `, + data, + config: { dataIdFromObject: getIdField }, + }); - // Make sure we updated the store with the new data - expect(queryManager.cache.extract()["5"]).toEqual({ - id: "5", - isPrivate: true, - }); - }) - .then(resolve, reject); - } - ); + expect(result.data).toEqual(data); - itAsync( - "runs a mutation and puts the result in the store", - (resolve, reject) => { - const data = { - makeListPrivate: { - id: "5", - isPrivate: true, - }, - }; + // Make sure we updated the store with the new data + expect(queryManager.cache.extract()["5"]).toEqual({ + id: "5", + isPrivate: true, + }); + }); - return mockMutation({ - reject, - mutation: gql` - mutation makeListPrivate { - makeListPrivate(id: "5") { - id - isPrivate - } - } - `, - data, - config: { dataIdFromObject: getIdField }, - }) - .then(({ result, queryManager }) => { - expect(result.data).toEqual(data); - - // Make sure we updated the store with the new data - expect(queryManager.cache.extract()["5"]).toEqual({ - id: "5", - isPrivate: true, - }); - }) - .then(resolve, reject); - } - ); + it("runs a mutation and puts the result in the store", async () => { + const data = { + makeListPrivate: { + id: "5", + isPrivate: true, + }, + }; - itAsync( - "runs a mutation and puts the result in the store with root key", - (resolve, reject) => { - const mutation = gql` + const { result, queryManager } = await mockMutation({ + mutation: gql` mutation makeListPrivate { makeListPrivate(id: "5") { id isPrivate } } - `; + `, + data, + config: { dataIdFromObject: getIdField }, + }); - const data = { - makeListPrivate: { - id: "5", - isPrivate: true, - }, - }; + expect(result.data).toEqual(data); - const queryManager = createQueryManager({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data }, - }).setOnError(reject), - config: { dataIdFromObject: getIdField }, - }); + // Make sure we updated the store with the new data + expect(queryManager.cache.extract()["5"]).toEqual({ + id: "5", + isPrivate: true, + }); + }); - return queryManager - .mutate({ - mutation, - }) - .then((result) => { - expect(result.data).toEqual(data); + it("runs a mutation and puts the result in the store with root key", async () => { + const mutation = gql` + mutation makeListPrivate { + makeListPrivate(id: "5") { + id + isPrivate + } + } + `; - // Make sure we updated the store with the new data - expect(queryManager.cache.extract()["5"]).toEqual({ - id: "5", - isPrivate: true, - }); - }) - .then(resolve, reject); - } - ); + const data = { + makeListPrivate: { + id: "5", + isPrivate: true, + }, + }; + + const queryManager = createQueryManager({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data }, + }), + config: { dataIdFromObject: getIdField }, + }); + + const result = await queryManager.mutate({ mutation }); + + expect(result.data).toEqual(data); + + // Make sure we updated the store with the new data + expect(queryManager.cache.extract()["5"]).toEqual({ + id: "5", + isPrivate: true, + }); + }); - itAsync(`doesn't return data while query is loading`, (resolve, reject) => { + it(`doesn't return data while query is loading`, async () => { const query1 = gql` { people_one(id: 1) { @@ -1781,14 +1607,11 @@ describe("QueryManager", () => { const observable1 = queryManager.watchQuery({ query: query1 }); const observable2 = queryManager.watchQuery({ query: query2 }); - return Promise.all([ - observableToPromise({ observable: observable1 }, (result) => - expect(result.data).toEqual(data1) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]).then(resolve, reject); + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); + + await expect(stream1).toEmitMatchedValue({ data: data1 }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); }); it("updates result of previous query if the result of a new query overlaps", async () => { @@ -1867,7 +1690,7 @@ describe("QueryManager", () => { await expect(stream).not.toEmitAnything(); }); - itAsync("warns if you forget the template literal tag", async (resolve) => { + it("warns if you forget the template literal tag", async () => { const queryManager = mockQueryManager(); expect(() => { void queryManager.query({ @@ -1889,57 +1712,49 @@ describe("QueryManager", () => { query: "string" as any as DocumentNode, }); }).toThrowError(/wrap the query string in a "gql" tag/); - - resolve(); }); - itAsync( - "should transform queries correctly when given a QueryTransformer", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should transform queries correctly when given a QueryTransformer", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const transformedQuery = gql` - query { - author { - firstName - lastName - __typename - } + } + `; + const transformedQuery = gql` + query { + author { + firstName + lastName + __typename } - `; + } + `; - const transformedQueryResult = { - author: { - firstName: "John", - lastName: "Smith", - __typename: "Author", - }, - }; + const transformedQueryResult = { + author: { + firstName: "John", + lastName: "Smith", + __typename: "Author", + }, + }; - //make sure that the query is transformed within the query - //manager - createQueryManager({ - link: mockSingleLink({ - request: { query: transformedQuery }, - result: { data: transformedQueryResult }, - }).setOnError(reject), - config: { addTypename: true }, - }) - .query({ query: query }) - .then((result) => { - expect(result.data).toEqual(transformedQueryResult); - }) - .then(resolve, reject); - } - ); + //make sure that the query is transformed within the query + //manager + const result = await createQueryManager({ + link: mockSingleLink({ + request: { query: transformedQuery }, + result: { data: transformedQueryResult }, + }), + config: { addTypename: true }, + }).query({ query: query }); + + expect(result.data).toEqual(transformedQueryResult); + }); - itAsync("should transform mutations correctly", (resolve, reject) => { + it("should transform mutations correctly", async () => { const mutation = gql` mutation { createAuthor(firstName: "John", lastName: "Smith") { @@ -1966,476 +1781,380 @@ describe("QueryManager", () => { }, }; - createQueryManager({ + const result = await createQueryManager({ link: mockSingleLink({ request: { query: transformedMutation }, result: { data: transformedMutationResult }, - }).setOnError(reject), + }), config: { addTypename: true }, - }) - .mutate({ mutation: mutation }) - .then((result) => { - expect(result.data).toEqual(transformedMutationResult); - resolve(); - }); + }).mutate({ mutation: mutation }); + + expect(result.data).toEqual(transformedMutationResult); }); - itAsync( - "should reject a query promise given a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should reject a query promise given a network error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const networkError = new Error("Network error"); + } + `; + const networkError = new Error("Network error"); + + await expect( mockQueryManager({ request: { query }, error: networkError, - }) - .query({ query }) - .then(() => { - reject(new Error("Returned result on an errored fetchQuery")); - }) - .catch((error) => { - const apolloError = error as ApolloError; - - expect(apolloError.message).toBeDefined(); - expect(apolloError.networkError).toBe(networkError); - expect(apolloError.graphQLErrors).toEqual([]); - resolve(); - }) - .then(resolve, reject); - } - ); + }).query({ query }) + ).rejects.toEqual(new ApolloError({ networkError })); + }); - itAsync( - "should reject a query promise given a GraphQL error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should reject a query promise given a GraphQL error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const graphQLErrors = [new GraphQLError("GraphQL error")]; - return mockQueryManager({ + } + `; + const graphQLErrors = [new GraphQLError("GraphQL error")]; + await expect( + mockQueryManager({ request: { query }, result: { errors: graphQLErrors }, - }) - .query({ query }) - .then( - () => { - throw new Error("Returned result on an errored fetchQuery"); - }, - // don't use .catch() for this or it will catch the above error - (error) => { - const apolloError = error as ApolloError; - expect(apolloError.graphQLErrors).toEqual(graphQLErrors); - expect(!apolloError.networkError).toBeTruthy(); - } - ) - .then(resolve, reject); - } - ); + }).query({ query }) + ).rejects.toEqual(new ApolloError({ graphQLErrors })); + }); - itAsync( - "should not empty the store when a non-polling query fails due to a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "Dhaivat", - lastName: "Pandya", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - error: new Error("Network error ocurred"), + it("should not empty the store when a non-polling query fails due to a network error", async () => { + const query = gql` + query { + author { + firstName + lastName } - ); - queryManager - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - - queryManager - .query({ query, fetchPolicy: "network-only" }) - .then(() => { - reject( - new Error("Returned a result when it was not supposed to.") - ); - }) - .catch(() => { - // make that the error thrown doesn't empty the state - expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( - data.author - ); - resolve(); - }); - }) - .catch(() => { - reject(new Error("Threw an error on the first query.")); - }); - } - ); + } + `; + const data = { + author: { + firstName: "Dhaivat", + lastName: "Pandya", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + error: new Error("Network error ocurred"), + } + ); + const result = await queryManager.query({ query }); - itAsync( - "should be able to unsubscribe from a polling query subscription", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + expect(result.data).toEqual(data); + + await expect( + queryManager.query({ query, fetchPolicy: "network-only" }) + ).rejects.toThrow(); + + expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( + data.author + ); + }); + + it("should be able to unsubscribe from a polling query subscription", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const observable = mockQueryManager({ + const observable = mockQueryManager( + { request: { query }, result: { data }, - }).watchQuery({ query, pollInterval: 20 }); - - const { promise, subscription } = observableToPromiseAndSubscription( - { - observable, - wait: 60, + }, + { + request: { query }, + result: () => { + throw new Error("Should not again"); }, - (result: any) => { - expect(result.data).toEqual(data); - subscription.unsubscribe(); - } - ); + } + ).watchQuery({ query, pollInterval: 20 }); + const stream = new ObservableStream(observable); - return promise.then(resolve, reject); - } - ); + await expect(stream).toEmitMatchedValue({ data }); - itAsync( - "should not empty the store when a polling query fails due to a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + stream.unsubscribe(); + + // Ensure polling has stopped by ensuring the error is not thrown from the mocks + await wait(30); + }); + + it("should not empty the store when a polling query fails due to a network error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - error: new Error("Network error occurred."), - } - ); - const observable = queryManager.watchQuery({ - query, - pollInterval: 20, - notifyOnNetworkStatusChange: false, - }); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + error: new Error("Network error occurred."), + } + ); + const observable = queryManager.watchQuery({ + query, + pollInterval: 20, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - return observableToPromise( - { - observable, - errorCallbacks: [ - () => { - expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( - data.author - ); - }, - ], - }, - (result) => { - expect(result.data).toEqual(data); - expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( - data.author - ); + await expect(stream).toEmitMatchedValue({ data }); + expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( + data.author + ); + + await expect(stream).toEmitError( + new ApolloError({ networkError: new Error("Network error occurred.") }) + ); + expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( + data.author + ); + }); + + it("should not fire next on an observer if there is no change in the result", async () => { + const query = gql` + query { + author { + firstName + lastName } - ).then(resolve, reject); - } - ); + } + `; - itAsync( - "should not fire next on an observer if there is no change in the result", - (resolve, reject) => { - const query = gql` - query { - author { + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data }, + } + ); + + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + const result = await queryManager.query({ query }); + expect(result.data).toEqual(data); + + await expect(stream).not.toEmitAnything(); + }); + + it("should not return stale data when we orphan a real-id node in the store with a real-id node", async () => { + const query1 = gql` + query { + author { + name { firstName lastName } + age + id + __typename + } + } + `; + const query2 = gql` + query { + author { + name { + firstName + } + id + __typename } - `; - - const data = { - author: { + } + `; + const data1 = { + author: { + name: { firstName: "John", lastName: "Smith", }, - }; - const queryManager = mockQueryManager( + age: 18, + id: "187", + __typename: "Author", + }, + }; + const data2 = { + author: { + name: { + firstName: "John", + }, + id: "197", + __typename: "Author", + }, + }; + const reducerConfig = { dataIdFromObject }; + const queryManager = createQueryManager({ + link: mockSingleLink( { - request: { query }, - result: { data }, + request: { query: query1 }, + result: { data: data1 }, }, { - request: { query }, - result: { data }, + request: { query: query2 }, + result: { data: data2 }, + }, + { + request: { query: query1 }, + result: { data: data1 }, } - ); + ), + config: reducerConfig, + }); - const observable = queryManager.watchQuery({ query }); - return Promise.all([ - // we wait for a little bit to ensure the result of the second query - // don't trigger another subscription event - observableToPromise({ observable, wait: 100 }, (result) => { - expect(result.data).toEqual(data); - }), - queryManager.query({ query }).then((result) => { - expect(result.data).toEqual(data); - }), - ]).then(resolve, reject); - } - ); + const observable1 = queryManager.watchQuery({ query: query1 }); + const observable2 = queryManager.watchQuery({ query: query2 }); - itAsync( - "should not return stale data when we orphan a real-id node in the store with a real-id node", - (resolve, reject) => { - const query1 = gql` - query { - author { - name { - firstName - lastName - } - age - id - __typename + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); + + await expect(stream1).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + await expect(stream2).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); + + it("should return partial data when configured when we orphan a real-id node in the store with a real-id node", async () => { + const query1 = gql` + query { + author { + name { + firstName + lastName } + age + id + __typename } - `; - const query2 = gql` - query { - author { - name { - firstName - } - id - __typename + } + `; + const query2 = gql` + query { + author { + name { + firstName } + id + __typename } - `; - const data1 = { - author: { - name: { - firstName: "John", - lastName: "Smith", - }, - age: 18, - id: "187", - __typename: "Author", + } + `; + const data1 = { + author: { + name: { + firstName: "John", + lastName: "Smith", }, - }; - const data2 = { - author: { - name: { - firstName: "John", - }, - id: "197", - __typename: "Author", + age: 18, + id: "187", + __typename: "Author", + }, + }; + const data2 = { + author: { + name: { + firstName: "John", }, - }; - const reducerConfig = { dataIdFromObject }; - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query: query1 }, - result: { data: data1 }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query: query1 }, - result: { data: data1 }, - } - ).setOnError(reject), - config: reducerConfig, - }); + id: "197", + __typename: "Author", + }, + }; - const observable1 = queryManager.watchQuery({ query: query1 }); - const observable2 = queryManager.watchQuery({ query: query2 }); + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query: query1 }, + result: { data: data1 }, + }, + { + request: { query: query2 }, + result: { data: data2 }, + } + ), + }); - // I'm not sure the waiting 60 here really is required, but the test used to do it - return Promise.all([ - observableToPromise( - { - observable: observable1, - wait: 60, - }, - (result) => { - expect(result).toEqual({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - observableToPromise( - { - observable: observable2, - wait: 60, - }, - (result) => { - expect(result).toEqual({ - data: data2, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - ]).then(resolve, reject); - } - ); + const observable1 = queryManager.watchQuery({ + query: query1, + returnPartialData: true, + }); + const observable2 = queryManager.watchQuery({ query: query2 }); - itAsync( - "should return partial data when configured when we orphan a real-id node in the store with a real-id node", - (resolve, reject) => { - const query1 = gql` - query { - author { - name { - firstName - lastName - } - age - id - __typename - } - } - `; - const query2 = gql` - query { - author { - name { - firstName - } - id - __typename - } - } - `; - const data1 = { - author: { - name: { - firstName: "John", - lastName: "Smith", - }, - age: 18, - id: "187", - __typename: "Author", - }, - }; - const data2 = { - author: { - name: { - firstName: "John", - }, - id: "197", - __typename: "Author", - }, - }; + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query: query1 }, - result: { data: data1 }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - } - ).setOnError(reject), - }); - - const observable1 = queryManager.watchQuery({ - query: query1, - returnPartialData: true, - }); - const observable2 = queryManager.watchQuery({ query: query2 }); - - return Promise.all([ - observableToPromise( - { - observable: observable1, - }, - (result) => { - expect(result).toEqual({ - data: {}, - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - }, - (result) => { - expect(result).toEqual({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - observableToPromise( - { - observable: observable2, - }, - (result) => { - expect(result).toEqual({ - data: data2, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - ]).then(resolve, reject); - } - ); + await expect(stream1).toEmitValue({ + data: {}, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream2).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + await expect(stream1).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); it("should not write unchanged network results to cache", async () => { const cache = new InMemoryCache({ @@ -2656,254 +2375,233 @@ describe("QueryManager", () => { await expect(stream).not.toEmitAnything(); }); - itAsync( - "should not error when replacing unidentified data with a normalized ID", - (resolve, reject) => { - const queryWithoutId = gql` - query { - author { - name { - firstName - lastName - } - age - __typename + it("should not error when replacing unidentified data with a normalized ID", async () => { + const queryWithoutId = gql` + query { + author { + name { + firstName + lastName } + age + __typename } - `; + } + `; - const queryWithId = gql` - query { - author { - name { - firstName - } - id - __typename + const queryWithId = gql` + query { + author { + name { + firstName } + id + __typename } - `; + } + `; - const dataWithoutId = { - author: { - name: { - firstName: "John", - lastName: "Smith", - }, - age: "124", - __typename: "Author", + const dataWithoutId = { + author: { + name: { + firstName: "John", + lastName: "Smith", }, - }; + age: "124", + __typename: "Author", + }, + }; - const dataWithId = { - author: { - name: { - firstName: "Jane", - }, - id: "129", - __typename: "Author", + const dataWithId = { + author: { + name: { + firstName: "Jane", }, - }; + id: "129", + __typename: "Author", + }, + }; - let mergeCount = 0; - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query: queryWithoutId }, - result: { data: dataWithoutId }, - }, - { - request: { query: queryWithId }, - result: { data: dataWithId }, - } - ).setOnError(reject), - config: { - typePolicies: { - Query: { - fields: { - author: { - merge(existing, incoming, { isReference, readField }) { - switch (++mergeCount) { - case 1: - expect(existing).toBeUndefined(); - expect(isReference(incoming)).toBe(false); - expect(incoming).toEqual(dataWithoutId.author); - break; - case 2: - expect(existing).toEqual(dataWithoutId.author); - expect(isReference(incoming)).toBe(true); - expect(readField("id", incoming)).toBe("129"); - expect(readField("name", incoming)).toEqual( - dataWithId.author.name - ); - break; - default: - fail("unreached"); - } - return incoming; - }, + let mergeCount = 0; + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query: queryWithoutId }, + result: { data: dataWithoutId }, + }, + { + request: { query: queryWithId }, + result: { data: dataWithId }, + } + ), + config: { + typePolicies: { + Query: { + fields: { + author: { + merge(existing, incoming, { isReference, readField }) { + switch (++mergeCount) { + case 1: + expect(existing).toBeUndefined(); + expect(isReference(incoming)).toBe(false); + expect(incoming).toEqual(dataWithoutId.author); + break; + case 2: + expect(existing).toEqual(dataWithoutId.author); + expect(isReference(incoming)).toBe(true); + expect(readField("id", incoming)).toBe("129"); + expect(readField("name", incoming)).toEqual( + dataWithId.author.name + ); + break; + default: + fail("unreached"); + } + return incoming; }, }, }, }, }, - }); + }, + }); - const observableWithId = queryManager.watchQuery({ - query: queryWithId, - }); + const observableWithId = queryManager.watchQuery({ + query: queryWithId, + }); - const observableWithoutId = queryManager.watchQuery({ - query: queryWithoutId, - }); + const observableWithoutId = queryManager.watchQuery({ + query: queryWithoutId, + }); - return Promise.all([ - observableToPromise({ observable: observableWithoutId }, (result) => - expect(result.data).toEqual(dataWithoutId) - ), - observableToPromise({ observable: observableWithId }, (result) => - expect(result.data).toEqual(dataWithId) - ), - ]).then(resolve, reject); - } - ); + const stream1 = new ObservableStream(observableWithoutId); + const stream2 = new ObservableStream(observableWithId); - itAsync( - "exposes errors on a refetch as a rejection", - async (resolve, reject) => { - const request = { - query: gql` - { - people_one(id: 1) { - name - } + await expect(stream1).toEmitMatchedValue({ data: dataWithoutId }); + await expect(stream2).toEmitMatchedValue({ data: dataWithId }); + }); + + it("exposes errors on a refetch as a rejection", async () => { + const request = { + query: gql` + { + people_one(id: 1) { + name } - `, - }; - const firstResult = { - data: { - people_one: { - name: "Luke Skywalker", - }, + } + `, + }; + const firstResult = { + data: { + people_one: { + name: "Luke Skywalker", }, - }; - const secondResult = { - errors: [ - new GraphQLError("This is not the person you are looking for."), - ], - }; + }, + }; + const secondResult = { + errors: [new GraphQLError("This is not the person you are looking for.")], + }; - const queryManager = mockRefetch({ - request, - firstResult, - secondResult, - }); + const queryManager = mockRefetch({ + request, + firstResult, + secondResult, + }); - const handle = queryManager.watchQuery(request); + const handle = queryManager.watchQuery(request); + const stream = new ObservableStream(handle); - const checkError = (error: ApolloError) => { - expect(error.graphQLErrors[0].message).toEqual( - "This is not the person you are looking for." - ); - }; + await expect(stream).toEmitValue({ + data: firstResult.data, + loading: false, + networkStatus: NetworkStatus.ready, + }); - handle.subscribe({ - error: checkError, - }); + const expectedError = new ApolloError({ + graphQLErrors: secondResult.errors, + }); - handle - .refetch() - .then(() => { - reject(new Error("Error on refetch should reject promise")); - }) - .catch((error) => { - checkError(error); - }) - .then(resolve, reject); - } - ); - - itAsync( - "does not return incomplete data when two queries for the same item are executed", - (resolve, reject) => { - const queryA = gql` - query queryA { - person(id: "abc") { - __typename - id - firstName - lastName - } + await expect(handle.refetch()).rejects.toThrow(expectedError); + await expect(stream).toEmitError(expectedError); + }); + + it("does not return incomplete data when two queries for the same item are executed", async () => { + const queryA = gql` + query queryA { + person(id: "abc") { + __typename + id + firstName + lastName } - `; - const queryB = gql` - query queryB { - person(id: "abc") { - __typename - id - lastName - age - } + } + `; + const queryB = gql` + query queryB { + person(id: "abc") { + __typename + id + lastName + age } - `; - const dataA = { - person: { - __typename: "Person", - id: "abc", - firstName: "Luke", - lastName: "Skywalker", - }, - }; - const dataB = { - person: { - __typename: "Person", - id: "abc", - lastName: "Skywalker", - age: "32", - }, - }; - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link: mockSingleLink( - { request: { query: queryA }, result: { data: dataA } }, - { request: { query: queryB }, result: { data: dataB }, delay: 20 } - ).setOnError(reject), - cache: new InMemoryCache({}), - ssrMode: true, - }) - ); + } + `; + const dataA = { + person: { + __typename: "Person", + id: "abc", + firstName: "Luke", + lastName: "Skywalker", + }, + }; + const dataB = { + person: { + __typename: "Person", + id: "abc", + lastName: "Skywalker", + age: "32", + }, + }; + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link: mockSingleLink( + { request: { query: queryA }, result: { data: dataA } }, + { request: { query: queryB }, result: { data: dataB }, delay: 20 } + ), + cache: new InMemoryCache({}), + ssrMode: true, + }) + ); - const observableA = queryManager.watchQuery({ - query: queryA, - }); - const observableB = queryManager.watchQuery({ - query: queryB, - }); + const observableA = queryManager.watchQuery({ + query: queryA, + }); + const observableB = queryManager.watchQuery({ + query: queryB, + }); + const streamA = new ObservableStream(observableA); + const streamB = new ObservableStream(observableB); - return Promise.all([ - observableToPromise({ observable: observableA }, () => { - expect(getCurrentQueryResult(observableA)).toEqual({ - data: dataA, - partial: false, - }); - expect(getCurrentQueryResult(observableB)).toEqual({ - data: undefined, - partial: true, - }); - }), - observableToPromise({ observable: observableB }, () => { - expect(getCurrentQueryResult(observableA)).toEqual({ - data: dataA, - partial: false, - }); - expect(getCurrentQueryResult(observableB)).toEqual({ - data: dataB, - partial: false, - }); - }), - ]).then(resolve, reject); - } - ); + await expect(streamA).toEmitNext(); + expect(getCurrentQueryResult(observableA)).toEqual({ + data: dataA, + partial: false, + }); + expect(getCurrentQueryResult(observableB)).toEqual({ + data: undefined, + partial: true, + }); + + await expect(streamB).toEmitNext(); + expect(getCurrentQueryResult(observableA)).toEqual({ + data: dataA, + partial: false, + }); + expect(getCurrentQueryResult(observableB)).toEqual({ + data: dataB, + partial: false, + }); + }); it('only increments "queryInfo.lastRequestId" when fetching data from network', async () => { const query = gql` @@ -2966,7 +2664,7 @@ describe("QueryManager", () => { }); describe("polling queries", () => { - itAsync("allows you to poll queries", (resolve, reject) => { + it("allows you to poll queries", async () => { const query = gql` query fetchLuke($id: String) { people_one(id: $id) { @@ -3007,15 +2705,13 @@ describe("QueryManager", () => { pollInterval: 50, notifyOnNetworkStatusChange: false, }); + const stream = new ObservableStream(observable); - return observableToPromise( - { observable }, - (result) => expect(result.data).toEqual(data1), - (result) => expect(result.data).toEqual(data2) - ).then(resolve, reject); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitMatchedValue({ data: data2 }); }); - itAsync("does not poll during SSR", (resolve, reject) => { + it("does not poll during SSR", async () => { const query = gql` query fetchLuke($id: String) { people_one(id: $id) { @@ -3055,7 +2751,7 @@ describe("QueryManager", () => { request: { query, variables }, result: { data: data2 }, } - ).setOnError(reject), + ), cache: new InMemoryCache({ addTypename: false }), ssrMode: true, }) @@ -3067,286 +2763,250 @@ describe("QueryManager", () => { pollInterval: 10, notifyOnNetworkStatusChange: false, }); + const stream = new ObservableStream(observable); - let count = 1; - const subHandle = observable.subscribe({ - next: (result: any) => { - switch (count) { - case 1: - expect(result.data).toEqual(data1); - setTimeout(() => { - subHandle.unsubscribe(); - resolve(); - }, 15); - count++; - break; - case 2: - default: - reject(new Error("Only expected one result, not multiple")); - } - }, - }); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).not.toEmitAnything(); }); - itAsync( - "should let you handle multiple polled queries and unsubscribe from one of them", - (resolve) => { - const query1 = gql` - query { - author { - firstName - lastName - } + it("should let you handle multiple polled queries and unsubscribe from one of them", async () => { + const query1 = gql` + query { + author { + firstName + lastName } - `; - const query2 = gql` - query { - person { - name - } + } + `; + const query2 = gql` + query { + person { + name } - `; - const data11 = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const data12 = { - author: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const data13 = { - author: { - firstName: "Jolly", - lastName: "Smith", - }, - }; - const data14 = { - author: { - firstName: "Jared", - lastName: "Smith", - }, - }; - const data21 = { - person: { - name: "Jane Smith", - }, - }; - const data22 = { - person: { - name: "Josey Smith", - }, - }; - const queryManager = mockQueryManager( - { - request: { query: query1 }, - result: { data: data11 }, - }, - { - request: { query: query1 }, - result: { data: data12 }, - }, - { - request: { query: query1 }, - result: { data: data13 }, - }, - { - request: { query: query1 }, - result: { data: data14 }, - }, - { - request: { query: query2 }, - result: { data: data21 }, + } + `; + const data11 = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const data12 = { + author: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const data13 = { + author: { + firstName: "Jolly", + lastName: "Smith", + }, + }; + const data14 = { + author: { + firstName: "Jared", + lastName: "Smith", + }, + }; + const data21 = { + person: { + name: "Jane Smith", + }, + }; + const data22 = { + person: { + name: "Josey Smith", + }, + }; + const queryManager = mockQueryManager( + { + request: { query: query1 }, + result: { data: data11 }, + }, + { + request: { query: query1 }, + result: { data: data12 }, + }, + { + request: { query: query1 }, + result: { data: data13 }, + }, + { + request: { query: query1 }, + result: { data: data14 }, + }, + { + request: { query: query2 }, + result: { data: data21 }, + }, + { + request: { query: query2 }, + result: { data: data22 }, + } + ); + let handle1Count = 0; + let handleCount = 0; + let setMilestone = false; + + const subscription1 = queryManager + .watchQuery({ + query: query1, + pollInterval: 150, + }) + .subscribe({ + next() { + handle1Count++; + handleCount++; + if (handle1Count > 1 && !setMilestone) { + subscription1.unsubscribe(); + setMilestone = true; + } }, - { - request: { query: query2 }, - result: { data: data22 }, - } - ); - let handle1Count = 0; - let handleCount = 0; - let setMilestone = false; - - const subscription1 = queryManager - .watchQuery({ - query: query1, - pollInterval: 150, - }) - .subscribe({ - next() { - handle1Count++; - handleCount++; - if (handle1Count > 1 && !setMilestone) { - subscription1.unsubscribe(); - setMilestone = true; - } - }, - }); + }); - const subscription2 = queryManager - .watchQuery({ - query: query2, - pollInterval: 2000, - }) - .subscribe({ - next() { - handleCount++; - }, - }); + const subscription2 = queryManager + .watchQuery({ + query: query2, + pollInterval: 2000, + }) + .subscribe({ + next() { + handleCount++; + }, + }); - setTimeout(() => { - expect(handleCount).toBe(3); - subscription1.unsubscribe(); - subscription2.unsubscribe(); + await wait(400); - resolve(); - }, 400); - } - ); + expect(handleCount).toBe(3); + subscription1.unsubscribe(); + subscription2.unsubscribe(); + }); - itAsync( - "allows you to unsubscribe from polled queries", - (resolve, reject) => { - const query = gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name - } + it("allows you to unsubscribe from polled queries", async () => { + const query = gql` + query fetchLuke($id: String) { + people_one(id: $id) { + name } - `; + } + `; - const variables = { - id: "1", - }; + const variables = { + id: "1", + }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: data1 }, + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: data1 }, + }, + { + request: { query, variables }, + result: { data: data2 }, + }, + { + request: { query, variables }, + result: () => { + throw new Error("Should not fetch again"); }, - { - request: { query, variables }, - result: { data: data2 }, - } - ); - const observable = queryManager.watchQuery({ - query, - variables, - pollInterval: 50, - notifyOnNetworkStatusChange: false, - }); + } + ); + const observable = queryManager.watchQuery({ + query, + variables, + pollInterval: 50, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - const { promise, subscription } = observableToPromiseAndSubscription( - { - observable, - wait: 60, - }, - (result) => expect(result.data).toEqual(data1), - (result) => { - expect(result.data).toEqual(data2); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitMatchedValue({ data: data2 }); - // we unsubscribe here manually, rather than waiting for the timeout. - subscription.unsubscribe(); - } - ); + stream.unsubscribe(); - return promise.then(resolve, reject); - } - ); + // Ensure polling has stopped by ensuring the error is not thrown from the mocks + await wait(60); + }); - itAsync( - "allows you to unsubscribe from polled query errors", - (resolve, reject) => { - const query = gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name - } + it("allows you to unsubscribe from polled query errors", async () => { + const query = gql` + query fetchLuke($id: String) { + people_one(id: $id) { + name } - `; + } + `; - const variables = { - id: "1", - }; + const variables = { + id: "1", + }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: data1 }, - }, - { - request: { query, variables }, - error: new Error("Network error"), + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: data1 }, + }, + { + request: { query, variables }, + error: new Error("Network error"), + }, + { + request: { query, variables }, + result: { data: data2 }, + }, + { + request: { query, variables }, + result: () => { + throw new Error("Should not fetch again"); }, - { - request: { query, variables }, - result: { data: data2 }, - } - ); + } + ); - const observable = queryManager.watchQuery({ - query, - variables, - pollInterval: 50, - notifyOnNetworkStatusChange: false, - }); + const observable = queryManager.watchQuery({ + query, + variables, + pollInterval: 50, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - let isFinished = false; - process.once("unhandledRejection", () => { - if (!isFinished) reject("unhandledRejection from network"); - }); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitError( + new ApolloError({ networkError: new Error("Network error") }) + ); - const { promise, subscription } = observableToPromiseAndSubscription( - { - observable, - wait: 60, - errorCallbacks: [ - (error) => { - expect(error.message).toMatch("Network error"); - subscription.unsubscribe(); - }, - ], - }, - (result) => expect(result.data).toEqual(data1) - ); - - promise.then(() => { - setTimeout(() => { - isFinished = true; - resolve(); - }, 4); - }); - } - ); + stream.unsubscribe(); - itAsync("exposes a way to start a polling query", (resolve, reject) => { + // Ensure polling has stopped by ensuring the error is not thrown from the mocks + await wait(60); + }); + + it("exposes a way to start a polling query", async () => { const query = gql` query fetchLuke($id: String) { people_one(id: $id) { @@ -3388,15 +3048,13 @@ describe("QueryManager", () => { notifyOnNetworkStatusChange: false, }); observable.startPolling(50); + const stream = new ObservableStream(observable); - return observableToPromise( - { observable }, - (result) => expect(result.data).toEqual(data1), - (result) => expect(result.data).toEqual(data2) - ).then(resolve, reject); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitMatchedValue({ data: data2 }); }); - itAsync("exposes a way to stop a polling query", (resolve, reject) => { + it("exposes a way to stop a polling query", async () => { const query = gql` query fetchLeia($id: String) { people_one(id: $id) { @@ -3436,14 +3094,16 @@ describe("QueryManager", () => { variables, pollInterval: 50, }); + const stream = new ObservableStream(observable); - return observableToPromise({ observable, wait: 60 }, (result) => { - expect(result.data).toEqual(data1); - observable.stopPolling(); - }).then(resolve, reject); + await expect(stream).toEmitMatchedValue({ data: data1 }); + + observable.stopPolling(); + + await expect(stream).not.toEmitAnything(); }); - itAsync("stopped polling queries still get updates", (resolve, reject) => { + it("stopped polling queries still get updates", async () => { const query = gql` query fetchLeia($id: String) { people_one(id: $id) { @@ -3484,148 +3144,123 @@ describe("QueryManager", () => { variables, pollInterval: 50, }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data: data1 }); + + const result = await queryManager.query({ + query, + variables, + fetchPolicy: "network-only", + }); - return Promise.all([ - observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - queryManager - .query({ - query, - variables, - fetchPolicy: "network-only", - }) - .then((result) => { - expect(result.data).toEqual(data2); - }) - .catch(reject); - }, - (result) => { - expect(result.data).toEqual(data2); - } - ), - ]).then(resolve, reject); + expect(result.data).toEqual(data2); + await expect(stream).toEmitMatchedValue({ data: data2 }); }); }); + describe("store resets", () => { - itAsync( - "returns a promise resolving when all queries have been refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("returns a promise resolving when all queries have been refetched", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const dataChanged = { - author: { - firstName: "John changed", - lastName: "Smith", - }, - }; + const dataChanged = { + author: { + firstName: "John changed", + lastName: "Smith", + }, + }; - const query2 = gql` - query { - author2 { - firstName - lastName - } + const query2 = gql` + query { + author2 { + firstName + lastName } - `; + } + `; - const data2 = { - author2: { - firstName: "John", - lastName: "Smith", - }, - }; + const data2 = { + author2: { + firstName: "John", + lastName: "Smith", + }, + }; + + const data2Changed = { + author2: { + firstName: "John changed", + lastName: "Smith", + }, + }; - const data2Changed = { - author2: { - firstName: "John changed", - lastName: "Smith", + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query }, + result: { data }, }, - }; + { + request: { query: query2 }, + result: { data: data2 }, + }, + { + request: { query }, + result: { data: dataChanged }, + }, + { + request: { query: query2 }, + result: { data: data2Changed }, + } + ), + }); - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query }, - result: { data }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: dataChanged }, - }, - { - request: { query: query2 }, - result: { data: data2Changed }, - } - ).setOnError(reject), - }); + const observable = queryManager.watchQuery({ query }); + const observable2 = queryManager.watchQuery({ query: query2 }); - const observable = queryManager.watchQuery({ query }); - const observable2 = queryManager.watchQuery({ query: query2 }); + const stream = new ObservableStream(observable); + const stream2 = new ObservableStream(observable2); - return Promise.all([ - observableToPromise({ observable }, (result) => - expect(result.data).toEqual(data) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]) - .then(() => { - observable.subscribe({ next: () => null }); - observable2.subscribe({ next: () => null }); - - return resetStore(queryManager).then(() => { - const result = getCurrentQueryResult(observable); - expect(result.partial).toBe(false); - expect(result.data).toEqual(dataChanged); - - const result2 = getCurrentQueryResult(observable2); - expect(result2.partial).toBe(false); - expect(result2.data).toEqual(data2Changed); - }); - }) - .then(resolve, reject); - } - ); + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); - itAsync( - "should change the store state to an empty state", - (resolve, reject) => { - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + await resetStore(queryManager); - resetStore(queryManager); + const result = getCurrentQueryResult(observable); + expect(result.partial).toBe(false); + expect(result.data).toEqual(dataChanged); - expect(queryManager.cache.extract()).toEqual({}); - expect(queryManager.getQueryStore()).toEqual({}); - expect(queryManager.mutationStore).toEqual({}); + const result2 = getCurrentQueryResult(observable2); + expect(result2.partial).toBe(false); + expect(result2.data).toEqual(data2Changed); + }); - resolve(); - } - ); + it("should change the store state to an empty state", () => { + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + void resetStore(queryManager); - xit("should only refetch once when we store reset", () => { + expect(queryManager.cache.extract()).toEqual({}); + expect(queryManager.getQueryStore()).toEqual({}); + expect(queryManager.mutationStore).toEqual({}); + }); + + it.skip("should only refetch once when we store reset", async () => { let queryManager: QueryManager; const query = gql` query { @@ -3665,25 +3300,22 @@ describe("QueryManager", () => { ); queryManager = createQueryManager({ link }); const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - // wait just to make sure the observable doesn't fire again - return observableToPromise( - { observable, wait: 0 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - // reset the store after data has returned - resetStore(queryManager); - }, - (result) => { - // only refetch once and make sure data has changed - expect(result.data).toEqual(data2); - expect(timesFired).toBe(2); - } - ); + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); + + // reset the store after data has returned + void resetStore(queryManager); + + // only refetch once and make sure data has changed + await expect(stream).toEmitMatchedValue({ data: data2 }); + expect(timesFired).toBe(2); + + await expect(stream).not.toEmitAnything(); }); - itAsync("should not refetch torn-down queries", (resolve) => { + it("should not refetch torn-down queries", async () => { let queryManager: QueryManager; let observable: ObservableQuery; const query = gql` @@ -3707,31 +3339,26 @@ describe("QueryManager", () => { new Observable((observer) => { timesFired += 1; observer.next({ data }); - return; }), ]); queryManager = createQueryManager({ link }); observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); - observableToPromise({ observable, wait: 0 }, (result) => - expect(result.data).toEqual(data) - ).then(() => { - expect(timesFired).toBe(1); + stream.unsubscribe(); - // at this point the observable query has been torn down - // because observableToPromise unsubscribe before resolving - resetStore(queryManager); + expect(timesFired).toBe(1); - setTimeout(() => { - expect(timesFired).toBe(1); + void resetStore(queryManager); + await wait(50); - resolve(); - }, 50); - }); + expect(timesFired).toBe(1); }); - itAsync("should not error when resetStore called", (resolve, reject) => { + it("should not error when resetStore called", async () => { const query = gql` query { author { @@ -3766,23 +3393,18 @@ describe("QueryManager", () => { query, notifyOnNetworkStatusChange: false, }); + const stream = new ObservableStream(observable); - // wait to make sure store reset happened - return observableToPromise( - { observable, wait: 20 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - resetStore(queryManager).catch(reject); - }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(2); - } - ).then(resolve, reject); + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); + + void resetStore(queryManager); + + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(2); }); - itAsync("should not error on a stopped query()", (resolve, reject) => { + it("should not error on a stopped query()", async () => { let queryManager: QueryManager; const query = gql` query { @@ -3810,403 +3432,184 @@ describe("QueryManager", () => { queryManager = createQueryManager({ link }); const queryId = "1"; - queryManager - .fetchQuery(queryId, { query }) - .catch((e) => reject("Exception thrown for stopped query")); + const promise = queryManager.fetchQuery(queryId, { query }); queryManager.removeQuery(queryId); - resetStore(queryManager).then(resolve, reject); + + await resetStore(queryManager); + // Ensure the promise doesn't reject + await Promise.race([wait(50), promise]); }); - itAsync( - "should throw an error on an inflight fetch query if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should throw an error on an inflight fetch query if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - delay: 10000, //i.e. forever - }); - queryManager - .fetchQuery("made up id", { query }) - .then(() => { - reject(new Error("Returned a result.")); - }) - .catch((error) => { - expect(error.message).toMatch("Store reset"); - resolve(); - }); - // Need to delay the reset at least until the fetchRequest method - // has had a chance to enter this request into fetchQueryRejectFns. - setTimeout(() => resetStore(queryManager), 100); - } - ); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + delay: 10000, //i.e. forever + }); + const promise = queryManager.fetchQuery("made up id", { query }); - itAsync( - "should call refetch on a mocked Observable if the store is reset", - (resolve) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - }); - const obs = queryManager.watchQuery({ query }); - obs.subscribe({}); - obs.refetch = resolve as any; + // Need to delay the reset at least until the fetchRequest method + // has had a chance to enter this request into fetchQueryRejectFns. + await wait(100); + void resetStore(queryManager); - resetStore(queryManager); - } - ); + await expect(promise).rejects.toThrow( + new InvariantError( + "Store reset while query was in flight (not completed in link chain)" + ) + ); + }); - itAsync( - "should not call refetch on a cache-only Observable if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should call refetch on a mocked Observable if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); - - const options = { - query, - fetchPolicy: "cache-only", - } as WatchQueryOptions; - - let refetchCount = 0; + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + }); + const obs = queryManager.watchQuery({ query }); + obs.subscribe({}); + obs.refetch = jest.fn(); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + void resetStore(queryManager); - resetStore(queryManager); + await wait(0); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + expect(obs.refetch).toHaveBeenCalledTimes(1); + }); - itAsync( - "should not call refetch on a standby Observable if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should not call refetch on a cache-only Observable if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); - const options = { - query, - fetchPolicy: "standby", - } as WatchQueryOptions; + const options = { + query, + fetchPolicy: "cache-only", + } as WatchQueryOptions; - let refetchCount = 0; + let refetchCount = 0; - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; - resetStore(queryManager); + void resetStore(queryManager); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + await wait(50); - itAsync( - "should not call refetch on a non-subscribed Observable if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + expect(refetchCount).toEqual(0); + }); + + it("should not call refetch on a standby Observable if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); - const options = { - query, - } as WatchQueryOptions; + const options = { + query, + fetchPolicy: "standby", + } as WatchQueryOptions; - let refetchCount = 0; + let refetchCount = 0; - const obs = queryManager.watchQuery(options); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; - resetStore(queryManager); + void resetStore(queryManager); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + await wait(50); - itAsync( - "should throw an error on an inflight query() if the store is reset", - (resolve, reject) => { - let queryManager: QueryManager; - const query = gql` - query { - author { - firstName - lastName - } + expect(refetchCount).toEqual(0); + }); + + it("should not call refetch on a non-subscribed Observable if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const link = new ApolloLink( - () => - new Observable((observer) => { - // reset the store as soon as we hear about the query - resetStore(queryManager); - observer.next({ data }); - return; - }) - ); + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); - queryManager = createQueryManager({ link }); - queryManager - .query({ query }) - .then(() => { - reject(new Error("query() gave results on a store reset")); - }) - .catch(() => { - resolve(); - }); - } - ); - }); - describe("refetching observed queries", () => { - itAsync( - "returns a promise resolving when all queries have been refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; + const options = { + query, + } as WatchQueryOptions; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + let refetchCount = 0; - const dataChanged = { - author: { - firstName: "John changed", - lastName: "Smith", - }, - }; + const obs = queryManager.watchQuery(options); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; - const query2 = gql` - query { - author2 { - firstName - lastName - } - } - `; + void resetStore(queryManager); - const data2 = { - author2: { - firstName: "John", - lastName: "Smith", - }, - }; + await wait(50); - const data2Changed = { - author2: { - firstName: "John changed", - lastName: "Smith", - }, - }; + expect(refetchCount).toEqual(0); + }); - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query }, - result: { data }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: dataChanged }, - }, - { - request: { query: query2 }, - result: { data: data2Changed }, - } - ).setOnError(reject), - }); - - const observable = queryManager.watchQuery({ query }); - const observable2 = queryManager.watchQuery({ query: query2 }); - - return Promise.all([ - observableToPromise({ observable }, (result) => - expect(result.data).toEqual(data) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]) - .then(() => { - observable.subscribe({ next: () => null }); - observable2.subscribe({ next: () => null }); - - return queryManager.reFetchObservableQueries().then(() => { - const result = getCurrentQueryResult(observable); - expect(result.partial).toBe(false); - expect(result.data).toEqual(dataChanged); - - const result2 = getCurrentQueryResult(observable2); - expect(result2.partial).toBe(false); - expect(result2.data).toEqual(data2Changed); - }); - }) - .then(resolve, reject); - } - ); - - itAsync( - "should only refetch once when we refetch observable queries", - (resolve, reject) => { - let queryManager: QueryManager; - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - - const data2 = { - author: { - firstName: "Johnny", - lastName: "Smith", - }, - }; - - let timesFired = 0; - const link: ApolloLink = new ApolloLink( - (op) => - new Observable((observer) => { - timesFired += 1; - if (timesFired > 1) { - observer.next({ data: data2 }); - } else { - observer.next({ data }); - } - observer.complete(); - return; - }) - ); - queryManager = createQueryManager({ link }); - const observable = queryManager.watchQuery({ query }); - - // wait just to make sure the observable doesn't fire again - return observableToPromise( - { observable, wait: 0 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - // refetch the observed queries after data has returned - queryManager.reFetchObservableQueries(); - }, - (result) => { - // only refetch once and make sure data has changed - expect(result.data).toEqual(data2); - expect(timesFired).toBe(2); - resolve(); - } - ).catch((e) => { - reject(e); - }); - } - ); - - itAsync("should not refetch torn-down queries", (resolve) => { + it("should throw an error on an inflight query() if the store is reset", async () => { let queryManager: QueryManager; - let observable: ObservableQuery; const query = gql` query { author { @@ -4215,582 +3618,1101 @@ describe("QueryManager", () => { } } `; + const data = { author: { firstName: "John", lastName: "Smith", }, }; - - let timesFired = 0; - const link: ApolloLink = ApolloLink.from([ + const link = new ApolloLink( () => new Observable((observer) => { - timesFired += 1; + // reset the store as soon as we hear about the query + void resetStore(queryManager); observer.next({ data }); return; - }), - ]); + }) + ); queryManager = createQueryManager({ link }); - observable = queryManager.watchQuery({ query }); - observableToPromise({ observable, wait: 0 }, (result) => - expect(result.data).toEqual(data) - ).then(() => { - expect(timesFired).toBe(1); + await expect(queryManager.query({ query })).rejects.toBeTruthy(); + }); + }); - // at this point the observable query has been torn down - // because observableToPromise unsubscribe before resolving - queryManager.reFetchObservableQueries(); + describe("refetching observed queries", () => { + it("returns a promise resolving when all queries have been refetched", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; - setTimeout(() => { - expect(timesFired).toBe(1); + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - resolve(); - }, 50); - }); - }); + const dataChanged = { + author: { + firstName: "John changed", + lastName: "Smith", + }, + }; - itAsync( - "should not error after reFetchObservableQueries", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + const query2 = gql` + query { + author2 { + firstName + lastName } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - - let timesFired = 0; - const link = ApolloLink.from([ - () => - new Observable((observer) => { - timesFired += 1; - observer.next({ data }); - observer.complete(); - }), - ]); + } + `; - const queryManager = createQueryManager({ link }); + const data2 = { + author2: { + firstName: "John", + lastName: "Smith", + }, + }; - const observable = queryManager.watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }); + const data2Changed = { + author2: { + firstName: "John changed", + lastName: "Smith", + }, + }; - // wait to make sure store reset happened - return observableToPromise( - { observable, wait: 20 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - queryManager.reFetchObservableQueries(); + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query }, + result: { data }, }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(2); - } - ).then(resolve, reject); - } - ); - - itAsync( - "should NOT throw an error on an inflight fetch query if the observable queries are refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", + { + request: { query: query2 }, + result: { data: data2 }, }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - delay: 100, - }); - queryManager - .fetchQuery("made up id", { query }) - .then(resolve) - .catch((error) => { - reject(new Error("Should not return an error")); - }); - queryManager.reFetchObservableQueries(); - } - ); - - itAsync( - "should call refetch on a mocked Observable if the observed queries are refetched", - (resolve) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", + { + request: { query }, + result: { data: dataChanged }, }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - }); - - const obs = queryManager.watchQuery({ query }); - obs.subscribe({}); - obs.refetch = resolve as any; - - queryManager.reFetchObservableQueries(); - } - ); - - itAsync( - "should not call refetch on a cache-only Observable if the observed queries are refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + { + request: { query: query2 }, + result: { data: data2Changed }, } - `; + ), + }); - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + const observable = queryManager.watchQuery({ query }); + const observable2 = queryManager.watchQuery({ query: query2 }); - const options = { - query, - fetchPolicy: "cache-only", - } as WatchQueryOptions; + const stream = new ObservableStream(observable); + const stream2 = new ObservableStream(observable2); - let refetchCount = 0; + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + await queryManager.reFetchObservableQueries(); - queryManager.reFetchObservableQueries(); + const result = getCurrentQueryResult(observable); + expect(result.partial).toBe(false); + expect(result.data).toEqual(dataChanged); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + const result2 = getCurrentQueryResult(observable2); + expect(result2.partial).toBe(false); + expect(result2.data).toEqual(data2Changed); + }); - itAsync( - "should not call refetch on a standby Observable if the observed queries are refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should only refetch once when we refetch observable queries", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const options = { - query, - fetchPolicy: "standby", - } as WatchQueryOptions; + const data2 = { + author: { + firstName: "Johnny", + lastName: "Smith", + }, + }; - let refetchCount = 0; + let timesFired = 0; + const link: ApolloLink = new ApolloLink( + (op) => + new Observable((observer) => { + timesFired += 1; + if (timesFired > 1) { + observer.next({ data: data2 }); + } else { + observer.next({ data }); + } + observer.complete(); + return; + }) + ); + const queryManager = createQueryManager({ link }); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); - queryManager.reFetchObservableQueries(); + // refetch the observed queries after data has returned + void queryManager.reFetchObservableQueries(); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + await expect(stream).toEmitMatchedValue({ data: data2 }); + expect(timesFired).toBe(2); + }); - itAsync( - "should refetch on a standby Observable if the observed queries are refetched and the includeStandby parameter is set to true", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should not refetch torn-down queries", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const options = { - query, - fetchPolicy: "standby", - } as WatchQueryOptions; + let timesFired = 0; + const link: ApolloLink = ApolloLink.from([ + () => + new Observable((observer) => { + timesFired += 1; + observer.next({ data }); + return; + }), + ]); - let refetchCount = 0; + const queryManager = createQueryManager({ link }); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); - const includeStandBy = true; - queryManager.reFetchObservableQueries(includeStandBy); + stream.unsubscribe(); + void queryManager.reFetchObservableQueries(); - setTimeout(() => { - expect(refetchCount).toEqual(1); - resolve(); - }, 50); - } - ); + await wait(50); - itAsync( - "should not call refetch on a non-subscribed Observable", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; + expect(timesFired).toBe(1); + }); - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + it("should not error after reFetchObservableQueries", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const options = { - query, - } as WatchQueryOptions; + let timesFired = 0; + const link = ApolloLink.from([ + () => + new Observable((observer) => { + timesFired += 1; + observer.next({ data }); + observer.complete(); + }), + ]); - let refetchCount = 0; + const queryManager = createQueryManager({ link }); - const obs = queryManager.watchQuery(options); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + const observable = queryManager.watchQuery({ + query, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - queryManager.reFetchObservableQueries(); + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + void queryManager.reFetchObservableQueries(); - itAsync( - "should NOT throw an error on an inflight query() if the observed queries are refetched", - (resolve, reject) => { - let queryManager: QueryManager; - const query = gql` - query { - author { - firstName - lastName - } - } - `; + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(2); - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const link = new ApolloLink( - () => - new Observable((observer) => { - // refetch observed queries as soon as we hear about the query - queryManager.reFetchObservableQueries(); - observer.next({ data }); - observer.complete(); - }) - ); + await expect(stream).not.toEmitAnything(); + }); - queryManager = createQueryManager({ link }); - queryManager - .query({ query }) - .then(() => { - resolve(); - }) - .catch((e) => { - reject( - new Error( - "query() should not throw error when refetching observed queriest" - ) - ); - }); - } - ); - }); + it("should NOT throw an error on an inflight fetch query if the observable queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + delay: 100, + }); + const promise = queryManager.fetchQuery("made up id", { query }); + void queryManager.reFetchObservableQueries(); - describe("refetching specified queries", () => { - itAsync( - "returns a promise resolving when all queries have been refetched", - (resolve, reject) => { - const query = gql` - query GetAuthor { - author { - firstName - lastName - } + await expect(promise).resolves.toBeTruthy(); + }); + + it("should call refetch on a mocked Observable if the observed queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + }); - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const obs = queryManager.watchQuery({ query }); + obs.subscribe({}); + obs.refetch = jest.fn(); - const dataChanged = { - author: { - firstName: "John changed", - lastName: "Smith", - }, - }; + void queryManager.reFetchObservableQueries(); - const query2 = gql` - query GetAuthor2 { - author2 { - firstName - lastName - } + await wait(0); + + expect(obs.refetch).toHaveBeenCalledTimes(1); + }); + + it("should not call refetch on a cache-only Observable if the observed queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data2 = { - author2: { - firstName: "John", - lastName: "Smith", - }, - }; + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); - const data2Changed = { - author2: { - firstName: "John changed", - lastName: "Smith", - }, - }; + const options = { + query, + fetchPolicy: "cache-only", + } as WatchQueryOptions; - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query }, - result: { data }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: dataChanged }, - }, - { - request: { query: query2 }, - result: { data: data2Changed }, - } - ).setOnError(reject), - }); + let refetchCount = 0; - const observable = queryManager.watchQuery({ query }); - const observable2 = queryManager.watchQuery({ query: query2 }); + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; - return Promise.all([ - observableToPromise({ observable }, (result) => - expect(result.data).toEqual(data) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]) - .then(() => { - observable.subscribe({ next: () => null }); - observable2.subscribe({ next: () => null }); - - const results: any[] = []; - queryManager - .refetchQueries({ - include: ["GetAuthor", "GetAuthor2"], - }) - .forEach((result) => results.push(result)); + void queryManager.reFetchObservableQueries(); - return Promise.all(results).then(() => { - const result = getCurrentQueryResult(observable); - expect(result.partial).toBe(false); - expect(result.data).toEqual(dataChanged); + await wait(50); - const result2 = getCurrentQueryResult(observable2); - expect(result2.partial).toBe(false); - expect(result2.data).toEqual(data2Changed); - }); - }) - .then(resolve, reject); - } - ); - }); + expect(refetchCount).toEqual(0); + }); - describe("loading state", () => { - itAsync( - "should be passed as false if we are not watching a query", - (resolve, reject) => { - const query = gql` - query { - fortuneCookie + it("should not call refetch on a standby Observable if the observed queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const data = { - fortuneCookie: "Buy it", - }; - return mockQueryManager({ - request: { query }, - result: { data }, - }) - .query({ query }) - .then((result) => { - expect(!result.loading).toBeTruthy(); - expect(result.data).toEqual(data); - }) - .then(resolve, reject); - } - ); + } + `; - itAsync( - "should be passed to the observer as true if we are returning partial data", - (resolve, reject) => { - const fortuneCookie = - "You must stick to your goal but rethink your approach"; - const primeQuery = gql` - query { - fortuneCookie + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + const options = { + query, + fetchPolicy: "standby", + } as WatchQueryOptions; + + let refetchCount = 0; + + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; + + void queryManager.reFetchObservableQueries(); + + await wait(50); + + expect(refetchCount).toEqual(0); + }); + + it("should refetch on a standby Observable if the observed queries are refetched and the includeStandby parameter is set to true", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const primeData = { fortuneCookie }; + } + `; - const author = { name: "John" }; - const query = gql` - query { - fortuneCookie - author { - name - } + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + const options = { + query, + fetchPolicy: "standby", + } as WatchQueryOptions; + + let refetchCount = 0; + + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; + + const includeStandBy = true; + void queryManager.reFetchObservableQueries(includeStandBy); + + await wait(50); + + expect(refetchCount).toEqual(1); + }); + + it("should not call refetch on a non-subscribed Observable", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const fullData = { fortuneCookie, author }; + } + `; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data: fullData }, - delay: 5, - }, - { - request: { query: primeQuery }, - result: { data: primeData }, + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + const options = { + query, + } as WatchQueryOptions; + + let refetchCount = 0; + + const obs = queryManager.watchQuery(options); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; + + void queryManager.reFetchObservableQueries(); + + await wait(50); + + expect(refetchCount).toEqual(0); + }); + + it("should NOT throw an error on an inflight query() if the observed queries are refetched", async () => { + let queryManager: QueryManager; + const query = gql` + query { + author { + firstName + lastName } - ); - - return queryManager - .query({ query: primeQuery }) - .then((primeResult) => { - const observable = queryManager.watchQuery({ - query, - returnPartialData: true, - }); - - return observableToPromise( - { observable }, - (result) => { - expect(result.loading).toBe(true); - expect(result.data).toEqual(primeData); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(fullData); - } - ); + } + `; + + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const link = new ApolloLink( + () => + new Observable((observer) => { + // refetch observed queries as soon as we hear about the query + void queryManager.reFetchObservableQueries(); + observer.next({ data }); + observer.complete(); }) - .then(resolve, reject); - } - ); + ); + + queryManager = createQueryManager({ link }); + + await expect(queryManager.query({ query })).resolves.toBeTruthy(); + }); + }); + + describe("refetching specified queries", () => { + it("returns a promise resolving when all queries have been refetched", async () => { + const query = gql` + query GetAuthor { + author { + firstName + lastName + } + } + `; + + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + + const dataChanged = { + author: { + firstName: "John changed", + lastName: "Smith", + }, + }; + + const query2 = gql` + query GetAuthor2 { + author2 { + firstName + lastName + } + } + `; + + const data2 = { + author2: { + firstName: "John", + lastName: "Smith", + }, + }; + + const data2Changed = { + author2: { + firstName: "John changed", + lastName: "Smith", + }, + }; + + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query }, + result: { data }, + }, + { + request: { query: query2 }, + result: { data: data2 }, + }, + { + request: { query }, + result: { data: dataChanged }, + }, + { + request: { query: query2 }, + result: { data: data2Changed }, + } + ), + }); + + const observable = queryManager.watchQuery({ query }); + const observable2 = queryManager.watchQuery({ query: query2 }); + + const stream = new ObservableStream(observable); + const stream2 = new ObservableStream(observable2); + + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); + + const results: any[] = []; + queryManager + .refetchQueries({ + include: ["GetAuthor", "GetAuthor2"], + }) + .forEach((result) => results.push(result)); + + await Promise.all(results); + + const result = getCurrentQueryResult(observable); + expect(result.partial).toBe(false); + expect(result.data).toEqual(dataChanged); + + const result2 = getCurrentQueryResult(observable2); + expect(result2.partial).toBe(false); + expect(result2.data).toEqual(data2Changed); + }); + }); + + describe("loading state", () => { + it("should be passed as false if we are not watching a query", async () => { + const query = gql` + query { + fortuneCookie + } + `; + const data = { + fortuneCookie: "Buy it", + }; + const result = await mockQueryManager({ + request: { query }, + result: { data }, + }).query({ query }); + + expect(result.loading).toBe(false); + expect(result.data).toEqual(data); + }); + + it("should be passed to the observer as true if we are returning partial data", async () => { + const fortuneCookie = + "You must stick to your goal but rethink your approach"; + const primeQuery = gql` + query { + fortuneCookie + } + `; + const primeData = { fortuneCookie }; + + const author = { name: "John" }; + const query = gql` + query { + fortuneCookie + author { + name + } + } + `; + const fullData = { fortuneCookie, author }; + + const queryManager = mockQueryManager( + { + request: { query }, + result: { data: fullData }, + delay: 5, + }, + { + request: { query: primeQuery }, + result: { data: primeData }, + } + ); + + await queryManager.query({ query: primeQuery }); + + const observable = queryManager.watchQuery({ + query, + returnPartialData: true, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: primeData, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream).toEmitValue({ + data: fullData, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); + + it("should be passed to the observer as false if we are returning all the data", async () => { + const stream = getObservableStream({ + query: gql` + query { + author { + firstName + lastName + } + } + `, + result: { + data: { + author: { + firstName: "John", + lastName: "Smith", + }, + }, + }, + }); + + await expect(stream).toEmitValue({ + data: { author: { firstName: "John", lastName: "Smith" } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); + + it("will update on `resetStore`", async () => { + const testQuery = gql` + query { + author { + firstName + lastName + } + } + `; + const data1 = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const data2 = { + author: { + firstName: "John", + lastName: "Smith 2", + }, + }; + const queryManager = mockQueryManager( + { + request: { query: testQuery }, + result: { data: data1 }, + }, + { + request: { query: testQuery }, + result: { data: data2 }, + } + ); + + const stream = new ObservableStream( + queryManager.watchQuery({ + query: testQuery, + notifyOnNetworkStatusChange: false, + }) + ); + + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await wait(0); + void resetStore(queryManager); + + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); + }); + + it("will be true when partial data may be returned", async () => { + const query1 = gql` + { + a { + x1 + y1 + z1 + } + } + `; + const query2 = gql` + { + a { + x1 + y1 + z1 + } + b { + x2 + y2 + z2 + } + } + `; + const data1 = { + a: { x1: 1, y1: 2, z1: 3 }, + }; + const data2 = { + a: { x1: 1, y1: 2, z1: 3 }, + b: { x2: 3, y2: 2, z2: 1 }, + }; + const queryManager = mockQueryManager( + { + request: { query: query1 }, + result: { data: data1 }, + }, + { + request: { query: query2 }, + result: { data: data2 }, + delay: 5, + } + ); + + const result1 = await queryManager.query({ query: query1 }); + expect(result1.loading).toBe(false); + expect(result1.data).toEqual(data1); + + const observable = queryManager.watchQuery({ + query: query2, + returnPartialData: true, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: data1, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); + }); + }); + + describe("refetchQueries", () => { + let consoleWarnSpy: jest.SpyInstance; + beforeEach(() => { + consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); + }); + afterEach(() => { + consoleWarnSpy.mockRestore(); + }); + + it("should refetch the right query when a result is successfully returned", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const variables = { id: "1234" }; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + void queryManager.mutate({ mutation, refetchQueries: ["getAuthors"] }); + + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + }); + + it("should not warn and continue when an unknown query name is asked to refetch", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + const observable = queryManager.watchQuery({ + query, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + void queryManager.mutate({ + mutation, + refetchQueries: ["fakeQuery", "getAuthors"], + }); + + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + 'Unknown query named "%s" requested in refetchQueries options.include array', + "fakeQuery" + ); + }); + + it("should ignore (with warning) a query named in refetchQueries that has no active subscriptions", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + stream.unsubscribe(); + await queryManager.mutate({ + mutation, + refetchQueries: ["getAuthors"], + }); + + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + 'Unknown query named "%s" requested in refetchQueries options.include array', + "getAuthors" + ); + }); + + it("should ignore (with warning) a document node in refetchQueries that has no active subscriptions", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); - itAsync( - "should be passed to the observer as false if we are returning all the data", - (resolve, reject) => { - assertWithObserver({ - reject, - query: gql` - query { - author { - firstName - lastName - } - } - `, - result: { - data: { - author: { - firstName: "John", - lastName: "Smith", - }, - }, - }, - observer: { - next(result) { - expect(!result.loading).toBeTruthy(); - resolve(); - }, - }, - }); - } - ); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - itAsync("will update on `resetStore`", (resolve, reject) => { - const testQuery = gql` + await expect(stream).toEmitMatchedValue({ data }); + stream.unsubscribe(); + + // The subscription has been stopped already + await queryManager.mutate({ + mutation, + refetchQueries: [query], + }); + + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + 'Unknown query named "%s" requested in refetchQueries options.include array', + "getAuthors" + ); + }); + + it("should ignore (with warning) a document node containing an anonymous query in refetchQueries that has no active subscriptions", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` query { author { firstName @@ -4798,510 +4720,201 @@ describe("QueryManager", () => { } } `; - const data1 = { + const data = { author: { firstName: "John", lastName: "Smith", }, }; - const data2 = { + const secondReqData = { author: { - firstName: "John", - lastName: "Smith 2", + firstName: "Jane", + lastName: "Johnson", }, }; const queryManager = mockQueryManager( { - request: { query: testQuery }, - result: { data: data1 }, + request: { query }, + result: { data }, }, { - request: { query: testQuery }, - result: { data: data2 }, + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, } ); - let count = 0; - queryManager - .watchQuery({ - query: testQuery, - notifyOnNetworkStatusChange: false, - }) - .subscribe({ - next: (result) => { - switch (count++) { - case 0: - expect(result.loading).toBe(false); - expect(result.data).toEqual(data1); - setTimeout(() => { - resetStore(queryManager); - }, 0); - break; - case 1: - expect(result.loading).toBe(false); - expect(result.data).toEqual(data2); - resolve(); - break; - default: - reject(new Error("`next` was called to many times.")); - } - }, - error: (error) => reject(error), - }); - }); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - itAsync( - "will be true when partial data may be returned", - (resolve, reject) => { - const query1 = gql` - { - a { - x1 - y1 - z1 - } - } - `; - const query2 = gql` - { - a { - x1 - y1 - z1 - } - b { - x2 - y2 - z2 - } - } - `; - const data1 = { - a: { x1: 1, y1: 2, z1: 3 }, - }; - const data2 = { - a: { x1: 1, y1: 2, z1: 3 }, - b: { x2: 3, y2: 2, z2: 1 }, - }; - const queryManager = mockQueryManager( - { - request: { query: query1 }, - result: { data: data1 }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - delay: 5, - } - ); - - queryManager - .query({ query: query1 }) - .then((result1) => { - expect(result1.loading).toBe(false); - expect(result1.data).toEqual(data1); - - let count = 0; - queryManager - .watchQuery({ query: query2, returnPartialData: true }) - .subscribe({ - next: (result2) => { - switch (count++) { - case 0: - expect(result2.loading).toBe(true); - expect(result2.data).toEqual(data1); - break; - case 1: - expect(result2.loading).toBe(false); - expect(result2.data).toEqual(data2); - resolve(); - break; - default: - reject(new Error("`next` was called to many times.")); - } - }, - error: reject, - }); - }) - .then(resolve, reject); - } - ); - }); + await expect(stream).toEmitMatchedValue({ data }); + stream.unsubscribe(); - describe("refetchQueries", () => { - let consoleWarnSpy: jest.SpyInstance; - beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); - }); - afterEach(() => { - consoleWarnSpy.mockRestore(); + // The subscription has been stopped already + await queryManager.mutate({ + mutation, + refetchQueries: [query], + }); + + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + "Unknown anonymous query requested in refetchQueries options.include array" + ); }); - itAsync( - "should refetch the right query when a result is successfully returned", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const variables = { id: "1234" }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, + it("also works with a query document and variables", async () => { + const mutation = gql` + mutation changeAuthorName($id: ID!) { + changeAuthorName(newName: "Jack Smith", id: $id) { + firstName + lastName } - ); - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, - }); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ mutation, refetchQueries: ["getAuthors"] }); - }, - (result) => { - expect(observable.getCurrentResult().data).toEqual(secondReqData); - expect(result.data).toEqual(secondReqData); + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName } - ).then(resolve, reject); - } - ); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - itAsync( - "should not warn and continue when an unknown query name is asked to refetch", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); - const observable = queryManager.watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ - mutation, - refetchQueries: ["fakeQuery", "getAuthors"], - }); - }, - (result) => { - expect(result.data).toEqual(secondReqData); - expect(consoleWarnSpy).toHaveBeenLastCalledWith( - 'Unknown query named "%s" requested in refetchQueries options.include array', - "fakeQuery" - ); - } - ).then(resolve, reject); - } - ); + const variables = { id: "1234" }; + const mutationVariables = { id: "2345" }; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data }, + delay: 10, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + delay: 100, + }, + { + request: { query: mutation, variables: mutationVariables }, + result: { data: mutationData }, + delay: 10, + } + ); + const observable = queryManager.watchQuery({ query, variables }); + const stream = new ObservableStream(observable); - itAsync( - "should ignore (with warning) a query named in refetchQueries that has no active subscriptions", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); + await expect(stream).toEmitMatchedValue({ data }); + + await queryManager.mutate({ + mutation, + variables: mutationVariables, + refetchQueries: [{ query, variables }], + }); - const observable = queryManager.watchQuery({ query }); - return observableToPromise({ observable }, (result) => { - expect(result.data).toEqual(data); - }) - .then(() => { - // The subscription has been stopped already - return queryManager.mutate({ - mutation, - refetchQueries: ["getAuthors"], - }); - }) - .then(() => { - expect(consoleWarnSpy).toHaveBeenLastCalledWith( - 'Unknown query named "%s" requested in refetchQueries options.include array', - "getAuthors" - ); - }) - .then(resolve, reject); - } - ); + await expect(stream).toEmitMatchedValue( + { data: secondReqData }, + { timeout: 150 } + ); + expect(observable.getCurrentResult().data).toEqual(secondReqData); - itAsync( - "should ignore (with warning) a document node in refetchQueries that has no active subscriptions", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors { - author { - firstName - lastName - } + await expect(stream).not.toEmitAnything(); + }); + + it("also works with a query document node", async () => { + const mutation = gql` + mutation changeAuthorName($id: ID!) { + changeAuthorName(newName: "Jack Smith", id: $id) { + firstName + lastName } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName } - ); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - const observable = queryManager.watchQuery({ query }); - return observableToPromise({ observable }, (result) => { - expect(result.data).toEqual(data); - }) - .then(() => { - // The subscription has been stopped already - return queryManager.mutate({ - mutation, - refetchQueries: [query], - }); - }) - .then(() => { - expect(consoleWarnSpy).toHaveBeenLastCalledWith( - 'Unknown query named "%s" requested in refetchQueries options.include array', - "getAuthors" - ); - }) - .then(resolve, reject); - } - ); + const variables = { id: "1234" }; + const mutationVariables = { id: "2345" }; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data }, + delay: 10, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + delay: 100, + }, + { + request: { query: mutation, variables: mutationVariables }, + result: { data: mutationData }, + delay: 10, + } + ); + const observable = queryManager.watchQuery({ query, variables }); + const stream = new ObservableStream(observable); - itAsync( - "should ignore (with warning) a document node containing an anonymous query in refetchQueries that has no active subscriptions", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); + await expect(stream).toEmitMatchedValue({ data }); - const observable = queryManager.watchQuery({ query }); - return observableToPromise({ observable }, (result) => { - expect(result.data).toEqual(data); - }) - .then(() => { - // The subscription has been stopped already - return queryManager.mutate({ - mutation, - refetchQueries: [query], - }); - }) - .then(() => { - expect(consoleWarnSpy).toHaveBeenLastCalledWith( - "Unknown anonymous query requested in refetchQueries options.include array" - ); - }) - .then(resolve, reject); - } - ); + await queryManager.mutate({ + mutation, + variables: mutationVariables, + refetchQueries: [query], + }); - it("also works with a query document and variables", async () => { + await expect(stream).toEmitMatchedValue( + { data: secondReqData }, + { timeout: 150 } + ); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + + await expect(stream).not.toEmitAnything(); + }); + + it("also works with different references of a same query document node", async () => { const mutation = gql` mutation changeAuthorName($id: ID!) { changeAuthorName(newName: "Jack Smith", id: $id) { @@ -5364,22 +4977,151 @@ describe("QueryManager", () => { await queryManager.mutate({ mutation, variables: mutationVariables, - refetchQueries: [{ query, variables }], + // spread the query into a new object to simulate multiple instances + refetchQueries: [{ ...query }], }); await expect(stream).toEmitMatchedValue( { data: secondReqData }, { timeout: 150 } ); - expect(observable.getCurrentResult().data).toEqual(secondReqData); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + + await expect(stream).not.toEmitAnything(); + }); + + it("also works with a conditional function that returns false", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + const conditional = jest.fn(() => []); + await queryManager.mutate({ mutation, refetchQueries: conditional }); + + expect(conditional).toHaveBeenCalledTimes(1); + expect(conditional).toHaveBeenCalledWith( + expect.objectContaining({ data: mutationData }) + ); + }); + + it("also works with a conditional function that returns an array of refetches", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + const conditional = jest.fn(() => [{ query }]); + await queryManager.mutate({ mutation, refetchQueries: conditional }); + + expect(conditional).toHaveBeenCalledTimes(1); + expect(conditional).toHaveBeenCalledWith( + expect.objectContaining({ data: mutationData }) + ); - await expect(stream).not.toEmitAnything(); + await expect(stream).toEmitMatchedValue({ data: secondReqData }); }); - it("also works with a query document node", async () => { + it("should refetch using the original query context (if any)", async () => { const mutation = gql` - mutation changeAuthorName($id: ID!) { - changeAuthorName(newName: "Jack Smith", id: $id) { + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { firstName lastName } @@ -5411,50 +5153,55 @@ describe("QueryManager", () => { lastName: "Johnson", }, }; - const variables = { id: "1234" }; - const mutationVariables = { id: "2345" }; const queryManager = mockQueryManager( { request: { query, variables }, result: { data }, - delay: 10, }, { request: { query, variables }, result: { data: secondReqData }, - delay: 100, }, { - request: { query: mutation, variables: mutationVariables }, + request: { query: mutation }, result: { data: mutationData }, - delay: 10, } ); - const observable = queryManager.watchQuery({ query, variables }); + + const headers = { + someHeader: "some value", + }; + const observable = queryManager.watchQuery({ + query, + variables, + context: { + headers, + }, + notifyOnNetworkStatusChange: false, + }); const stream = new ObservableStream(observable); - await expect(stream).toEmitMatchedValue({ data }); + await expect(stream).toEmitNext(); - await queryManager.mutate({ + void queryManager.mutate({ mutation, - variables: mutationVariables, - refetchQueries: [query], + refetchQueries: ["getAuthors"], }); - await expect(stream).toEmitMatchedValue( - { data: secondReqData }, - { timeout: 150 } - ); - expect(observable.getCurrentResult().data).toEqual(secondReqData); + await expect(stream).toEmitNext(); - await expect(stream).not.toEmitAnything(); + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); }); - it("also works with different references of a same query document node", async () => { + it("should refetch using the specified context, if provided", async () => { const mutation = gql` - mutation changeAuthorName($id: ID!) { - changeAuthorName(newName: "Jack Smith", id: $id) { + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { firstName lastName } @@ -5486,881 +5233,714 @@ describe("QueryManager", () => { lastName: "Johnson", }, }; - const variables = { id: "1234" }; - const mutationVariables = { id: "2345" }; const queryManager = mockQueryManager( { request: { query, variables }, result: { data }, - delay: 10, }, { request: { query, variables }, result: { data: secondReqData }, - delay: 100, }, { - request: { query: mutation, variables: mutationVariables }, + request: { query: mutation }, result: { data: mutationData }, - delay: 10, } ); - const observable = queryManager.watchQuery({ query, variables }); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + const headers = { + someHeader: "some value", + }; + + await expect(stream).toEmitNext(); + + void queryManager.mutate({ + mutation, + refetchQueries: [ + { + query, + variables, + context: { + headers, + }, + }, + ], + }); + + await expect(stream).toEmitNext(); + + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); + }); + }); + + describe("onQueryUpdated", () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } + } + `; + + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + + const variables = { id: "1234" }; + + function makeQueryManager() { + return mockQueryManager( + { + request: { query, variables }, + result: { data }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + } + + it("should refetch the right query when a result is successfully returned", async () => { + const queryManager = makeQueryManager(); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); const stream = new ObservableStream(observable); + let finishedRefetch = false; + await expect(stream).toEmitMatchedValue({ data }); await queryManager.mutate({ mutation, - variables: mutationVariables, - // spread the query into a new object to simulate multiple instances - refetchQueries: [{ ...query }], + + update(cache) { + cache.modify({ + fields: { + author(_, { INVALIDATE }) { + return INVALIDATE; + }, + }, + }); + }, + + async onQueryUpdated(obsQuery) { + expect(obsQuery.options.query).toBe(query); + const result = await obsQuery.refetch(); + + // Wait a bit to make sure the mutation really awaited the + // refetching of the query. + await wait(100); + finishedRefetch = true; + return result; + }, }); - await expect(stream).toEmitMatchedValue( - { data: secondReqData }, - { timeout: 150 } - ); + expect(finishedRefetch).toBe(true); + await expect(stream).toEmitMatchedValue({ data: secondReqData }); expect(observable.getCurrentResult().data).toEqual(secondReqData); - - await expect(stream).not.toEmitAnything(); }); - itAsync( - "also works with a conditional function that returns false", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); - const observable = queryManager.watchQuery({ query }); - const conditional = (result: FetchResult) => { - expect(result.data).toEqual(mutationData); - return []; - }; + it("should refetch using the original query context (if any)", async () => { + const queryManager = makeQueryManager(); - return observableToPromise({ observable }, (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ mutation, refetchQueries: conditional }); - }).then(resolve, reject); - } - ); + const headers = { + someHeader: "some value", + }; - itAsync( - "also works with a conditional function that returns an array of refetches", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); - const observable = queryManager.watchQuery({ query }); - const conditional = (result: FetchResult) => { - expect(result.data).toEqual(mutationData); - return [{ query }]; - }; + const observable = queryManager.watchQuery({ + query, + variables, + context: { + headers, + }, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ mutation, refetchQueries: conditional }); - }, - (result) => expect(result.data).toEqual(secondReqData) - ).then(resolve, reject); - } - ); + await expect(stream).toEmitMatchedValue({ data }); - itAsync( - "should refetch using the original query context (if any)", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const variables = { id: "1234" }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); + void queryManager.mutate({ + mutation, + + update(cache) { + cache.modify({ + fields: { + author(_, { INVALIDATE }) { + return INVALIDATE; + }, + }, + }); + }, + + onQueryUpdated(obsQuery) { + expect(obsQuery.options.query).toBe(query); + return obsQuery.refetch(); + }, + }); + + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); + }); + + it("should refetch using the specified context, if provided", async () => { + const queryManager = makeQueryManager(); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + const headers = { + someHeader: "some value", + }; - const headers = { - someHeader: "some value", - }; - const observable = queryManager.watchQuery({ - query, - variables, - context: { - headers, - }, - notifyOnNetworkStatusChange: false, - }); + await expect(stream).toEmitMatchedValue({ data }); - return observableToPromise( - { observable }, - (result) => { - queryManager.mutate({ - mutation, - refetchQueries: ["getAuthors"], - }); - }, - (result) => { - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); + void queryManager.mutate({ + mutation, - itAsync( - "should refetch using the specified context, if provided", - (resolve, reject) => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } - } - `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; - const variables = { id: "1234" }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); + update(cache) { + cache.evict({ fieldName: "author" }); + }, - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, - }); + onQueryUpdated(obsQuery) { + expect(obsQuery.options.query).toBe(query); + return obsQuery.reobserve({ + fetchPolicy: "network-only", + context: { + ...obsQuery.options.context, + headers, + }, + }); + }, + }); - const headers = { - someHeader: "some value", - }; + await expect(stream).toEmitMatchedValue({ data: secondReqData }); - return observableToPromise( - { observable }, - (result) => { - queryManager.mutate({ - mutation, - refetchQueries: [ - { - query, - variables, - context: { - headers, - }, - }, - ], - }); - }, - (result) => { - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); + }); }); - describe("onQueryUpdated", () => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName + describe("awaitRefetchQueries", () => { + it("should not wait for `refetchQueries` to complete before resolving the mutation, when `awaitRefetchQueries` is undefined", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } } - } - `; + `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } } - } - `; + `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - const variables = { id: "1234" }; + const variables = { id: "1234" }; - function makeQueryManager() { - return mockQueryManager( + const queryManager = mockQueryManager( { request: { query, variables }, - result: { data }, + result: { data: queryData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, }, { request: { query, variables }, result: { data: secondReqData }, + } + ); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let mutationComplete = false; + + await expect(stream).toEmitMatchedValue({ data: queryData }); + + void queryManager + .mutate({ + mutation, + refetchQueries: ["getAuthors"], + awaitRefetchQueries: false, + }) + .then(() => { + mutationComplete = true; + }); + + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + expect(mutationComplete).toBe(true); + }); + + it("should not wait for `refetchQueries` to complete before resolving the mutation, when `awaitRefetchQueries` is false", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } + } + `; + + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + + const variables = { id: "1234" }; + + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: queryData }, }, { request: { query: mutation }, result: { data: mutationData }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, } ); - } - itAsync( - "should refetch the right query when a result is successfully returned", - (resolve, reject) => { - const queryManager = makeQueryManager(); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let mutationComplete = false; + + await expect(stream).toEmitMatchedValue({ data: queryData }); - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, + void queryManager + .mutate({ mutation, refetchQueries: ["getAuthors"] }) + .then(() => { + mutationComplete = true; }); - let finishedRefetch = false; - - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - - return queryManager - .mutate({ - mutation, - - update(cache) { - cache.modify({ - fields: { - author(_, { INVALIDATE }) { - return INVALIDATE; - }, - }, - }); - }, - - onQueryUpdated(obsQuery) { - expect(obsQuery.options.query).toBe(query); - return obsQuery.refetch().then(async (result) => { - // Wait a bit to make sure the mutation really awaited the - // refetching of the query. - await new Promise((resolve) => setTimeout(resolve, 100)); - finishedRefetch = true; - return result; - }); - }, - }) - .then(() => { - expect(finishedRefetch).toBe(true); - }); - }, + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + expect(mutationComplete).toBe(true); + }); - (result) => { - expect(observable.getCurrentResult().data).toEqual(secondReqData); - expect(result.data).toEqual(secondReqData); - expect(finishedRefetch).toBe(true); + it("should wait for `refetchQueries` to complete before resolving the mutation, when `awaitRefetchQueries` is `true`", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName } - ).then(resolve, reject); - } - ); + } + `; - itAsync( - "should refetch using the original query context (if any)", - (resolve, reject) => { - const queryManager = makeQueryManager(); + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const headers = { - someHeader: "some value", - }; + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; - const observable = queryManager.watchQuery({ - query, - variables, - context: { - headers, - }, - notifyOnNetworkStatusChange: false, - }); + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - - queryManager.mutate({ - mutation, - - update(cache) { - cache.modify({ - fields: { - author(_, { INVALIDATE }) { - return INVALIDATE; - }, - }, - }); - }, + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - onQueryUpdated(obsQuery) { - expect(obsQuery.options.query).toBe(query); - return obsQuery.refetch(); - }, - }); - }, + const variables = { id: "1234" }; + + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: queryData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + } + ); - (result) => { - expect(result.data).toEqual(secondReqData); - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let mutationComplete = false; - itAsync( - "should refetch using the specified context, if provided", - (resolve, reject) => { - const queryManager = makeQueryManager(); + await expect(stream).toEmitMatchedValue({ data: queryData }); - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, + void queryManager + .mutate({ + mutation, + refetchQueries: ["getAuthors"], + awaitRefetchQueries: true, + }) + .then(() => { + mutationComplete = true; }); - const headers = { - someHeader: "some value", - }; - - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - - queryManager.mutate({ - mutation, - - update(cache) { - cache.evict({ fieldName: "author" }); - }, - - onQueryUpdated(obsQuery) { - expect(obsQuery.options.query).toBe(query); - return obsQuery.reobserve({ - fetchPolicy: "network-only", - context: { - ...obsQuery.options.context, - headers, - }, - }); - }, - }); - }, - - (result) => { - expect(result.data).toEqual(secondReqData); - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); - }); + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + expect(mutationComplete).toBe(false); + }); - describe("awaitRefetchQueries", () => { - const awaitRefetchTest = ({ - awaitRefetchQueries, - testQueryError = false, - }: MutationBaseOptions & { testQueryError?: boolean }) => - new Promise((resolve, reject) => { - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName - } + it("should allow catching errors from `refetchQueries` when `awaitRefetchQueries` is `true`", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName } - `; + } + `; - const queryData = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName } - `; + } + `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - const variables = { id: "1234" }; + const variables = { id: "1234" }; + const refetchError = new Error("Refetch failed"); - const refetchError = - testQueryError ? new Error("Refetch failed") : undefined; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: queryData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + error: refetchError, + } + ); - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: queryData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - error: refetchError, - } - ); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let isRefetchErrorCaught = false; - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, + await expect(stream).toEmitMatchedValue({ data: queryData }); + + void queryManager + .mutate({ + mutation, + refetchQueries: ["getAuthors"], + awaitRefetchQueries: true, + }) + .catch((error) => { + expect(error).toBeDefined(); + isRefetchErrorCaught = true; }); - let isRefetchErrorCaught = false; - let mutationComplete = false; - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(queryData); - const mutateOptions: MutationOptions = { - mutation, - refetchQueries: ["getAuthors"], - }; - if (awaitRefetchQueries) { - mutateOptions.awaitRefetchQueries = awaitRefetchQueries; - } - queryManager - .mutate(mutateOptions) - .then(() => { - mutationComplete = true; - }) - .catch((error) => { - expect(error).toBeDefined(); - isRefetchErrorCaught = true; - }); - }, - (result) => { - if (awaitRefetchQueries) { - expect(mutationComplete).not.toBeTruthy(); - } else { - expect(mutationComplete).toBeTruthy(); - } - expect(observable.getCurrentResult().data).toEqual(secondReqData); - expect(result.data).toEqual(secondReqData); - } - ) - .then(() => resolve()) - .catch((error) => { - const isRefetchError = - awaitRefetchQueries && - testQueryError && - error.message.includes(refetchError?.message); - - if (isRefetchError) { - return setTimeout(() => { - expect(isRefetchErrorCaught).toBe(true); - resolve(); - }, 10); - } + await expect(stream).toEmitError( + new ApolloError({ networkError: refetchError }) + ); + expect(isRefetchErrorCaught).toBe(true); + }); + }); - reject(error); - }); - }); + describe("store watchers", () => { + it("does not fill up the store on resolved queries", async () => { + const query1 = gql` + query One { + one + } + `; + const query2 = gql` + query Two { + two + } + `; + const query3 = gql` + query Three { + three + } + `; + const query4 = gql` + query Four { + four + } + `; - it( - "should not wait for `refetchQueries` to complete before resolving " + - "the mutation, when `awaitRefetchQueries` is undefined", - () => awaitRefetchTest({ awaitRefetchQueries: void 0 }) - ); + const link = mockSingleLink( + { request: { query: query1 }, result: { data: { one: 1 } } }, + { request: { query: query2 }, result: { data: { two: 2 } } }, + { request: { query: query3 }, result: { data: { three: 3 } } }, + { request: { query: query4 }, result: { data: { four: 4 } } } + ); + const cache = new InMemoryCache(); - it( - "should not wait for `refetchQueries` to complete before resolving " + - "the mutation, when `awaitRefetchQueries` is false", - () => awaitRefetchTest({ awaitRefetchQueries: false }) - ); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache, + }) + ); - it( - "should wait for `refetchQueries` to complete before resolving " + - "the mutation, when `awaitRefetchQueries` is `true`", - () => awaitRefetchTest({ awaitRefetchQueries: true }) - ); + await queryManager.query({ query: query1 }); + await queryManager.query({ query: query2 }); + await queryManager.query({ query: query3 }); + await queryManager.query({ query: query4 }); + await wait(10); - it( - "should allow catching errors from `refetchQueries` when " + - "`awaitRefetchQueries` is `true`", - () => - awaitRefetchTest({ awaitRefetchQueries: true, testQueryError: true }) - ); + expect(cache["watches"].size).toBe(0); + }); }); - describe("store watchers", () => { - itAsync( - "does not fill up the store on resolved queries", - (resolve, reject) => { - const query1 = gql` - query One { - one - } - `; - const query2 = gql` - query Two { - two - } - `; - const query3 = gql` - query Three { - three - } - `; - const query4 = gql` - query Four { - four + describe("`no-cache` handling", () => { + it("should return a query result (if one exists) when a `no-cache` fetch policy is used", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - - const link = mockSingleLink( - { request: { query: query1 }, result: { data: { one: 1 } } }, - { request: { query: query2 }, result: { data: { two: 2 } } }, - { request: { query: query3 }, result: { data: { three: 3 } } }, - { request: { query: query4 }, result: { data: { four: 4 } } } - ).setOnError(reject); - const cache = new InMemoryCache(); - - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache, - }) - ); + } + `; - return queryManager - .query({ query: query1 }) - .then((one) => { - return queryManager.query({ query: query2 }); - }) - .then(() => { - return queryManager.query({ query: query3 }); - }) - .then(() => { - return queryManager.query({ query: query4 }); - }) - .then(() => { - return new Promise((r) => { - setTimeout(r, 10); - }); - }) - .then(() => { - // @ts-ignore - expect(cache.watches.size).toBe(0); - }) - .then(resolve, reject); - } - ); - }); + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - describe("`no-cache` handling", () => { - itAsync( - "should return a query result (if one exists) when a `no-cache` fetch policy is used", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; + const queryManager = createQueryManager({ + link: mockSingleLink({ + request: { query }, + result: { data }, + }), + }); - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const observable = queryManager.watchQuery({ + query, + fetchPolicy: "no-cache", + }); + const stream = new ObservableStream(observable); - const queryManager = createQueryManager({ - link: mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject), - }); + await expect(stream).toEmitMatchedValue({ data }); - const observable = queryManager.watchQuery({ - query, - fetchPolicy: "no-cache", - }); - observableToPromise({ observable }, (result) => { - expect(result.data).toEqual(data); - const currentResult = getCurrentQueryResult(observable); - expect(currentResult.data).toEqual(data); - resolve(); - }); - } - ); + const currentResult = getCurrentQueryResult(observable); + expect(currentResult.data).toEqual(data); + }); }); describe("client awareness", () => { - itAsync( - "should pass client awareness settings into the link chain via context", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should pass client awareness settings into the link chain via context", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); - const clientAwareness = { - name: "Test", - version: "1.0.0", - }; + const clientAwareness = { + name: "Test", + version: "1.0.0", + }; - const queryManager = createQueryManager({ - link, - clientAwareness, - }); + const queryManager = createQueryManager({ + link, + clientAwareness, + }); - const observable = queryManager.watchQuery({ - query, - fetchPolicy: "no-cache", - }); + const observable = queryManager.watchQuery({ + query, + fetchPolicy: "no-cache", + }); + const stream = new ObservableStream(observable); - observableToPromise({ observable }, (result) => { - const context = link.operation!.getContext(); - expect(context.clientAwareness).toBeDefined(); - expect(context.clientAwareness).toEqual(clientAwareness); - resolve(); - }); - } - ); + await expect(stream).toEmitNext(); + + const context = link.operation!.getContext(); + expect(context.clientAwareness).toBeDefined(); + expect(context.clientAwareness).toEqual(clientAwareness); + }); }); describe("queryDeduplication", () => { @@ -6383,7 +5963,7 @@ describe("QueryManager", () => { }), }); - queryManager.query({ query, context: { queryDeduplication: true } }); + void queryManager.query({ query, context: { queryDeduplication: true } }); expect( queryManager["inFlightLinkObservables"].peek(print(query), "{}") @@ -6434,11 +6014,9 @@ describe("QueryManager", () => { spy.mockRestore(); }); - function validateWarnings( - resolve: (result?: any) => void, - reject: (reason?: any) => void, - returnPartialData = false, - expectedWarnCount = 1 + async function validateWarnings( + returnPartialData: boolean, + expectedWarnCount: number ) { const query1 = gql` query { @@ -6484,38 +6062,34 @@ describe("QueryManager", () => { returnPartialData, }); - return observableToPromise({ observable: observable1 }, (result) => { - expect(result).toEqual({ - loading: false, - data: data1, - networkStatus: NetworkStatus.ready, - }); - }).then(() => { - observableToPromise({ observable: observable2 }, (result) => { - expect(result).toEqual({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - partial: true, - }); - expect(spy).toHaveBeenCalledTimes(expectedWarnCount); - }).then(resolve, reject); + const stream1 = new ObservableStream(observable1); + + await expect(stream1).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + stream1.unsubscribe(); + + const stream2 = new ObservableStream(observable2); + + await expect(stream2).toEmitMatchedValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + partial: true, }); + expect(spy).toHaveBeenCalledTimes(expectedWarnCount); } - itAsync( - "should show missing cache result fields warning when returnPartialData is false", - (resolve, reject) => { - validateWarnings(resolve, reject, false, 1); - } - ); + it("should show missing cache result fields warning when returnPartialData is false", async () => { + await validateWarnings(false, 1); + }); - itAsync( - "should not show missing cache result fields warning when returnPartialData is true", - (resolve, reject) => { - validateWarnings(resolve, reject, true, 0); - } - ); + it("should not show missing cache result fields warning when returnPartialData is true", async () => { + await validateWarnings(true, 0); + }); }); describe("defaultContext", () => { diff --git a/src/core/__tests__/QueryManager/multiple-results.ts b/src/core/__tests__/QueryManager/multiple-results.ts index 1d49bbb770b..a8458d0ff13 100644 --- a/src/core/__tests__/QueryManager/multiple-results.ts +++ b/src/core/__tests__/QueryManager/multiple-results.ts @@ -3,15 +3,17 @@ import gql from "graphql-tag"; import { InMemoryCache } from "../../../cache/inmemory/inMemoryCache"; // mocks -import { itAsync, MockSubscriptionLink } from "../../../testing/core"; +import { MockSubscriptionLink, wait } from "../../../testing/core"; // core import { QueryManager } from "../../QueryManager"; import { GraphQLError } from "graphql"; import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; +import { ObservableStream } from "../../../testing/internal"; +import { ApolloError } from "../../../errors"; describe("mutiple results", () => { - itAsync("allows multiple query results from link", (resolve, reject) => { + it("allows multiple query results from link", async () => { const query = gql` query LazyLoadLuke { people_one(id: 1) { @@ -49,102 +51,162 @@ describe("mutiple results", () => { query, variables: {}, }); + const stream = new ObservableStream(observable); - let count = 0; - observable.subscribe({ - next: (result) => { - count++; - if (count === 1) { - link.simulateResult({ result: { data: laterData } }); - } - if (count === 2) { - resolve(); + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitValue({ + data: laterData, + loading: false, + networkStatus: 7, + }); + }); + + it("allows multiple query results from link with ignored errors", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, }, - error: (e) => { - console.error(e); + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], }, + }; + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + const observable = queryManager.watchQuery({ + query, + variables: {}, + errorPolicy: "ignore", }); + const stream = new ObservableStream(observable); // fire off first result link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + link.simulateResult({ + result: { errors: [new GraphQLError("defer failed")] }, + }); + + await expect(stream).toEmitValueStrict({ + data: undefined, + loading: false, + networkStatus: 7, + }); + + await wait(20); + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitValue({ + data: laterData, + loading: false, + networkStatus: 7, + }); }); - itAsync( - "allows multiple query results from link with ignored errors", - (resolve, reject) => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { + it("strips errors from a result if ignored", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { name - friends @defer { - name - } } } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - errorPolicy: "ignore", - }); - - let count = 0; - observable.subscribe({ - next: (result) => { - // errors should never be passed since they are ignored - expect(result.errors).toBeUndefined(); - count++; - if (count === 1) { - // this shouldn't fire the next event again - link.simulateResult({ - result: { errors: [new GraphQLError("defer failed")] }, - }); - setTimeout(() => { - link.simulateResult({ result: { data: laterData } }); - }, 20); - } - if (count === 2) { - // make sure the count doesn't go up by accident - setTimeout(() => { - if (count === 3) throw new Error("error was not ignored"); - resolve(); - }); - } - }, - error: (e) => { - console.error(e); - }, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - } - ); - itAsync("strips errors from a result if ignored", (resolve, reject) => { + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + const observable = queryManager.watchQuery({ + query, + variables: {}, + errorPolicy: "ignore", + }); + const stream = new ObservableStream(observable); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + // this should fire the `next` event without this error + link.simulateResult({ + result: { + errors: [new GraphQLError("defer failed")], + data: laterData, + }, + }); + + await expect(stream).toEmitValueStrict({ + data: laterData, + loading: false, + networkStatus: 7, + }); + }); + + it.skip("allows multiple query results from link with all errors", async () => { const query = gql` query LazyLoadLuke { people_one(id: 1) { @@ -181,185 +243,105 @@ describe("mutiple results", () => { const observable = queryManager.watchQuery({ query, variables: {}, - errorPolicy: "ignore", + errorPolicy: "all", + }); + const stream = new ObservableStream(observable); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + // this should fire the next event again + link.simulateResult({ + error: new Error("defer failed"), + }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + errors: [new Error("defer failed")], + }); + + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitValueStrict({ + data: laterData, + loading: false, + networkStatus: 7, + }); + }); + + it("closes the observable if an error is set with the none policy", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + const observable = queryManager.watchQuery({ + query, + variables: {}, + // errorPolicy: 'none', // this is the default }); + const stream = new ObservableStream(observable); let count = 0; observable.subscribe({ next: (result) => { // errors should never be passed since they are ignored - expect(result.errors).toBeUndefined(); count++; - if (count === 1) { - expect(result.data).toEqual(initialData); - // this should fire the `next` event without this error - link.simulateResult({ - result: { - errors: [new GraphQLError("defer failed")], - data: laterData, - }, - }); + expect(result.errors).toBeUndefined(); } if (count === 2) { - expect(result.data).toEqual(laterData); - expect(result.errors).toBeUndefined(); - // make sure the count doesn't go up by accident - setTimeout(() => { - if (count === 3) reject(new Error("error was not ignored")); - resolve(); - }, 10); + console.log(new Error("result came after an error")); } }, error: (e) => { - console.error(e); + expect(e).toBeDefined(); + expect(e.graphQLErrors).toBeDefined(); }, }); // fire off first result link.simulateResult({ result: { data: initialData } }); - }); - itAsync.skip( - "allows multiple query results from link with all errors", - (resolve, reject) => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - errorPolicy: "all", - }); - - let count = 0; - observable.subscribe({ - next: (result) => { - try { - // errors should never be passed since they are ignored - count++; - if (count === 1) { - expect(result.errors).toBeUndefined(); - // this should fire the next event again - link.simulateResult({ - error: new Error("defer failed"), - }); - } - if (count === 2) { - expect(result.errors).toBeDefined(); - link.simulateResult({ result: { data: laterData } }); - } - if (count === 3) { - expect(result.errors).toBeUndefined(); - // make sure the count doesn't go up by accident - setTimeout(() => { - if (count === 4) reject(new Error("error was not ignored")); - resolve(); - }); - } - } catch (e) { - reject(e); - } - }, - error: (e) => { - reject(e); - }, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - } - ); - itAsync( - "closes the observable if an error is set with the none policy", - (resolve, reject) => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - // errorPolicy: 'none', // this is the default - }); - - let count = 0; - observable.subscribe({ - next: (result) => { - // errors should never be passed since they are ignored - count++; - if (count === 1) { - expect(result.errors).toBeUndefined(); - // this should fire the next event again - link.simulateResult({ - error: new Error("defer failed"), - }); - } - if (count === 2) { - console.log(new Error("result came after an error")); - } - }, - error: (e) => { - expect(e).toBeDefined(); - expect(e.graphQLErrors).toBeDefined(); - resolve(); - }, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - } - ); + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + link.simulateResult({ error: new Error("defer failed") }); + + await expect(stream).toEmitError( + new ApolloError({ networkError: new Error("defer failed") }) + ); + }); }); diff --git a/src/core/__tests__/QueryManager/recycler.ts b/src/core/__tests__/QueryManager/recycler.ts deleted file mode 100644 index fccddc901de..00000000000 --- a/src/core/__tests__/QueryManager/recycler.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * This test is used to verify the requirements for how react-apollo - * preserves observables using QueryRecycler. Eventually, QueryRecycler - * will be removed, but this test file should still be valid - */ - -// externals -import gql from "graphql-tag"; - -// core -import { QueryManager } from "../../QueryManager"; -import { ObservableQuery } from "../../ObservableQuery"; -import { ObservableSubscription } from "../../../utilities"; -import { itAsync } from "../../../testing"; -import { InMemoryCache } from "../../../cache"; - -// mocks -import { MockSubscriptionLink } from "../../../testing/core"; -import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; - -describe("Subscription lifecycles", () => { - itAsync( - "cleans up and reuses data like QueryRecycler wants", - (resolve, reject) => { - const query = gql` - query Luke { - people_one(id: 1) { - name - friends { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - // step 1, get some data - const observable = queryManager.watchQuery({ - query, - variables: {}, - fetchPolicy: "cache-and-network", - }); - - const observableQueries: Array<{ - observableQuery: ObservableQuery; - subscription: ObservableSubscription; - }> = []; - - const resubscribe = () => { - const { observableQuery, subscription } = observableQueries.pop()!; - subscription.unsubscribe(); - - observableQuery.setOptions({ - query, - fetchPolicy: "cache-and-network", - }); - - return observableQuery; - }; - - const sub = observable.subscribe({ - next(result: any) { - expect(result.loading).toBe(false); - expect(result.data).toEqual(initialData); - expect(observable.getCurrentResult().data).toEqual(initialData); - - // step 2, recycle it - observable.setOptions({ - fetchPolicy: "standby", - pollInterval: 0, - }); - - observableQueries.push({ - observableQuery: observable, - subscription: observable.subscribe({}), - }); - - // step 3, unsubscribe from observable - sub.unsubscribe(); - - setTimeout(() => { - // step 4, start new Subscription; - const recycled = resubscribe(); - const currentResult = recycled.getCurrentResult(); - expect(currentResult.data).toEqual(initialData); - resolve(); - }, 10); - }, - }); - - setInterval(() => { - // fire off first result - link.simulateResult({ result: { data: initialData } }); - }, 10); - } - ); -}); diff --git a/src/link/core/__tests__/ApolloLink.ts b/src/link/core/__tests__/ApolloLink.ts index 1a97d149c44..506968090dc 100644 --- a/src/link/core/__tests__/ApolloLink.ts +++ b/src/link/core/__tests__/ApolloLink.ts @@ -2,12 +2,12 @@ import gql from "graphql-tag"; import { print } from "graphql"; import { Observable } from "../../../utilities/observables/Observable"; -import { itAsync } from "../../../testing"; import { FetchResult, Operation, NextLink, GraphQLRequest } from "../types"; import { ApolloLink } from "../ApolloLink"; -import { DocumentNode } from "graphql"; +import { ObservableStream } from "../../../testing/internal"; +import { execute } from "../execute"; -export class SetContextLink extends ApolloLink { +class SetContextLink extends ApolloLink { constructor( private setContext: ( context: Record @@ -25,7 +25,7 @@ export class SetContextLink extends ApolloLink { } } -export const sampleQuery = gql` +const sampleQuery = gql` query SampleQuery { stub { id @@ -33,50 +33,11 @@ export const sampleQuery = gql` } `; -function checkCalls(calls: any[] = [], results: Array) { - expect(calls.length).toBe(results.length); - calls.map((call, i) => expect(call.data).toEqual(results[i])); -} - -interface TestResultType { - link: ApolloLink; - results?: any[]; - query?: DocumentNode; - done?: () => void; - context?: any; - variables?: any; -} - -export function testLinkResults(params: TestResultType) { - const { link, context, variables } = params; - const results = params.results || []; - const query = params.query || sampleQuery; - const done = params.done || (() => void 0); - - const spy = jest.fn(); - ApolloLink.execute(link, { query, context, variables }).subscribe({ - next: spy, - error: (error: any) => { - expect(error).toEqual(results.pop()); - checkCalls(spy.mock.calls[0], results); - if (done) { - done(); - } - }, - complete: () => { - checkCalls(spy.mock.calls[0], results); - if (done) { - done(); - } - }, - }); -} - -export const setContext = () => ({ add: 1 }); +const setContext = () => ({ add: 1 }); describe("ApolloClient", () => { describe("context", () => { - itAsync("should merge context when using a function", (resolve, reject) => { + it("should merge context when using a function", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op, forward) => { op.setContext((context: { add: number }) => ({ add: context.add + 2 })); @@ -91,70 +52,68 @@ describe("ApolloClient", () => { }); return Observable.of({ data: op.getContext().add }); }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync( - "should merge context when not using a function", - (resolve, reject) => { - const returnOne = new SetContextLink(setContext); - const mock = new ApolloLink((op, forward) => { - op.setContext({ add: 3 }); - op.setContext({ substract: 1 }); + it("should merge context when not using a function", async () => { + const returnOne = new SetContextLink(setContext); + const mock = new ApolloLink((op, forward) => { + op.setContext({ add: 3 }); + op.setContext({ substract: 1 }); - return forward(op); - }); - const link = returnOne.concat(mock).concat((op) => { - expect(op.getContext()).toEqual({ - add: 3, - substract: 1, - }); - return Observable.of({ data: op.getContext().add }); + return forward(op); + }); + const link = returnOne.concat(mock).concat((op) => { + expect(op.getContext()).toEqual({ + add: 3, + substract: 1, }); + return Observable.of({ data: op.getContext().add }); + }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link, - results: [3], - done: resolve, - }); - } - ); + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); + }); }); describe("concat", () => { - itAsync("should concat a function", (resolve, reject) => { + it("should concat a function", async () => { const returnOne = new SetContextLink(setContext); const link = returnOne.concat((operation, forward) => { return Observable.of({ data: { count: operation.getContext().add } }); }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link, - results: [{ count: 1 }], - done: resolve, - }); + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); }); - itAsync("should concat a Link", (resolve, reject) => { + it("should concat a Link", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op) => Observable.of({ data: op.getContext().add }) ); const link = returnOne.concat(mock); - testLinkResults({ - link, - results: [1], - done: resolve, - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 1 }); + await expect(stream).toComplete(); }); - itAsync("should pass error to observable's error", (resolve, reject) => { + it("should pass error to observable's error", async () => { const error = new Error("thrown"); const returnOne = new SetContextLink(setContext); const mock = new ApolloLink( @@ -166,14 +125,15 @@ describe("ApolloClient", () => { ); const link = returnOne.concat(mock); - testLinkResults({ - link, - results: [1, error], - done: resolve, - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 1 }); + await expect(stream).toEmitError(error); }); - itAsync("should concat a Link and function", (resolve, reject) => { + it("should concat a Link and function", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op, forward) => { op.setContext((context: { add: number }) => ({ add: context.add + 2 })); @@ -183,14 +143,15 @@ describe("ApolloClient", () => { return Observable.of({ data: op.getContext().add }); }); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync("should concat a function and Link", (resolve, reject) => { + it("should concat a function and Link", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op, forward) => Observable.of({ data: op.getContext().add }) @@ -204,14 +165,16 @@ describe("ApolloClient", () => { return forward(operation); }) .concat(mock); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync("should concat two functions", (resolve, reject) => { + it("should concat two functions", async () => { const returnOne = new SetContextLink(setContext); const link = returnOne .concat((operation, forward) => { @@ -221,14 +184,16 @@ describe("ApolloClient", () => { return forward(operation); }) .concat((op, forward) => Observable.of({ data: op.getContext().add })); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync("should concat two Links", (resolve, reject) => { + it("should concat two Links", async () => { const returnOne = new SetContextLink(setContext); const mock1 = new ApolloLink((operation, forward) => { operation.setContext({ @@ -241,88 +206,93 @@ describe("ApolloClient", () => { ); const link = returnOne.concat(mock1).concat(mock2); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync( - "should return an link that can be concat'd multiple times", - (resolve, reject) => { - const returnOne = new SetContextLink(setContext); - const mock1 = new ApolloLink((operation, forward) => { - operation.setContext({ - add: operation.getContext().add + 2, - }); - return forward(operation); + it("should return an link that can be concat'd multiple times", async () => { + const returnOne = new SetContextLink(setContext); + const mock1 = new ApolloLink((operation, forward) => { + operation.setContext({ + add: operation.getContext().add + 2, }); - const mock2 = new ApolloLink((op, forward) => - Observable.of({ data: op.getContext().add + 2 }) + return forward(operation); + }); + const mock2 = new ApolloLink((op, forward) => + Observable.of({ data: op.getContext().add + 2 }) + ); + const mock3 = new ApolloLink((op, forward) => + Observable.of({ data: op.getContext().add + 3 }) + ); + const link = returnOne.concat(mock1); + + { + const stream = new ObservableStream( + execute(link.concat(mock2), { query: sampleQuery }) ); - const mock3 = new ApolloLink((op, forward) => - Observable.of({ data: op.getContext().add + 3 }) + + await expect(stream).toEmitValue({ data: 5 }); + await expect(stream).toComplete(); + } + + { + const stream = new ObservableStream( + execute(link.concat(mock3), { query: sampleQuery }) ); - const link = returnOne.concat(mock1); - testLinkResults({ - link: link.concat(mock2), - results: [5], - }); - testLinkResults({ - link: link.concat(mock3), - results: [6], - done: resolve, - }); + await expect(stream).toEmitValue({ data: 6 }); + await expect(stream).toComplete(); } - ); + }); }); describe("empty", () => { - itAsync( - "should returns an immediately completed Observable", - (resolve, reject) => { - testLinkResults({ - link: ApolloLink.empty(), - done: resolve, - }); - } - ); + it("should returns an immediately completed Observable", async () => { + const stream = new ObservableStream( + execute(ApolloLink.empty(), { query: sampleQuery }) + ); + + await expect(stream).toComplete(); + }); }); describe("execute", () => { - itAsync( - "transforms an opearation with context into something serlizable", - (resolve, reject) => { - const query = gql` - { - id - } - `; - const link = new ApolloLink((operation) => { - const str = JSON.stringify({ - ...operation, - query: print(operation.query), - }); - - expect(str).toBe( - JSON.stringify({ - variables: { id: 1 }, - extensions: { cache: true }, - query: print(operation.query), - }) - ); - return Observable.of(); + it("transforms an opearation with context into something serlizable", async () => { + const query = gql` + { + id + } + `; + const link = new ApolloLink((operation) => { + const str = JSON.stringify({ + ...operation, + query: print(operation.query), }); - const noop = () => {}; - ApolloLink.execute(link, { + + expect(str).toBe( + JSON.stringify({ + variables: { id: 1 }, + extensions: { cache: true }, + query: print(operation.query), + }) + ); + return Observable.of(); + }); + const stream = new ObservableStream( + execute(link, { query, variables: { id: 1 }, extensions: { cache: true }, - }).subscribe(noop, noop, resolve); - } - ); + }) + ); + + await expect(stream).toComplete(); + }); describe("execute", () => { let _warn: (message?: any, ...originalParams: any[]) => void; @@ -340,92 +310,87 @@ describe("ApolloClient", () => { console.warn = _warn; }); - itAsync( - "should return an empty observable when a link returns null", - (resolve, reject) => { - const link = new ApolloLink(); - link.request = () => null; - testLinkResults({ - link, - results: [], - done: resolve, - }); - } - ); + it("should return an empty observable when a link returns null", async () => { + const link = new ApolloLink(); + link.request = () => null; - itAsync( - "should return an empty observable when a link is empty", - (resolve, reject) => { - testLinkResults({ - link: ApolloLink.empty(), - results: [], - done: resolve, - }); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - itAsync( - "should return an empty observable when a concat'd link returns null", - (resolve, reject) => { - const link = new ApolloLink((operation, forward) => { - return forward(operation); - }).concat(() => null); - testLinkResults({ - link, - results: [], - done: resolve, - }); - } - ); + await expect(stream).toComplete(); + }); + + it("should return an empty observable when a link is empty", async () => { + const stream = new ObservableStream( + execute(ApolloLink.empty(), { query: sampleQuery }) + ); + + await expect(stream).toComplete(); + }); + + it("should return an empty observable when a concat'd link returns null", async () => { + const link = new ApolloLink((operation, forward) => { + return forward(operation); + }).concat(() => null); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toComplete(); + }); - itAsync( - "should return an empty observable when a split link returns null", - (resolve, reject) => { - let context = { test: true }; - const link = new SetContextLink(() => context).split( - (op) => op.getContext().test, - () => Observable.of(), - () => null + it("should return an empty observable when a split link returns null", async () => { + let context = { test: true }; + const link = new SetContextLink(() => context).split( + (op) => op.getContext().test, + () => Observable.of(), + () => null + ); + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) ); - testLinkResults({ - link, - results: [], - }); - context.test = false; - testLinkResults({ - link, - results: [], - done: resolve, - }); + + await expect(stream).toComplete(); } - ); - itAsync( - "should set a default context, variable, and query on a copy of operation", - (resolve, reject) => { - const operation = { - query: gql` - { - id - } - `, - }; - const link = new ApolloLink((op: Operation) => { - expect((operation as any)["operationName"]).toBeUndefined(); - expect((operation as any)["variables"]).toBeUndefined(); - expect((operation as any)["context"]).toBeUndefined(); - expect((operation as any)["extensions"]).toBeUndefined(); - expect(op["variables"]).toBeDefined(); - expect((op as any)["context"]).toBeUndefined(); - expect(op["extensions"]).toBeDefined(); - return Observable.of(); - }); + context.test = false; - ApolloLink.execute(link, operation).subscribe({ - complete: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toComplete(); } - ); + }); + + it("should set a default context, variable, and query on a copy of operation", async () => { + const operation = { + query: gql` + { + id + } + `, + }; + const link = new ApolloLink((op: Operation) => { + expect((operation as any)["operationName"]).toBeUndefined(); + expect((operation as any)["variables"]).toBeUndefined(); + expect((operation as any)["context"]).toBeUndefined(); + expect((operation as any)["extensions"]).toBeUndefined(); + expect(op["variables"]).toBeDefined(); + expect((op as any)["context"]).toBeUndefined(); + expect(op["extensions"]).toBeDefined(); + return Observable.of(); + }); + + const stream = new ObservableStream(execute(link, operation)); + + await expect(stream).toComplete(); + }); }); }); @@ -437,19 +402,14 @@ describe("ApolloClient", () => { extensions: {}, }; - itAsync( - "should create an observable that completes when passed an empty array", - (resolve, reject) => { - const observable = ApolloLink.execute(ApolloLink.from([]), { - query: sampleQuery, - }); - observable.subscribe( - () => expect(false), - () => expect(false), - resolve - ); - } - ); + it("should create an observable that completes when passed an empty array", async () => { + const observable = ApolloLink.execute(ApolloLink.from([]), { + query: sampleQuery, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toComplete(); + }); it("can create chain of one", () => { expect(() => ApolloLink.from([new ApolloLink()])).not.toThrow(); @@ -464,7 +424,7 @@ describe("ApolloClient", () => { ).not.toThrow(); }); - itAsync("should receive result of one link", (resolve, reject) => { + it("should receive result of one link", async () => { const data: FetchResult = { data: { hello: "world", @@ -475,15 +435,10 @@ describe("ApolloClient", () => { ]); // Smoke tests execute as a static method const observable = ApolloLink.execute(chain, uniqueOperation); - observable.subscribe({ - next: (actualData) => { - expect(data).toEqual(actualData); - }, - error: () => { - throw new Error(); - }, - complete: () => resolve(), - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(data); + await expect(stream).toComplete(); }); it("should accept AST query and pass AST to link", () => { @@ -497,7 +452,7 @@ describe("ApolloClient", () => { const chain = ApolloLink.from([new ApolloLink(stub)]); ApolloLink.execute(chain, astOperation); - expect(stub).toBeCalledWith({ + expect(stub).toHaveBeenCalledWith({ query: sampleQuery, operationName: "SampleQuery", variables: {}, @@ -505,157 +460,131 @@ describe("ApolloClient", () => { }); }); - itAsync( - "should pass operation from one link to next with modifications", - (resolve, reject) => { - const chain = ApolloLink.from([ - new ApolloLink((op, forward) => - forward({ - ...op, - query: sampleQuery, - }) - ), - new ApolloLink((op) => { - expect({ - extensions: {}, - operationName: "SampleQuery", - query: sampleQuery, - variables: {}, - }).toEqual(op); - - resolve(); - - return new Observable((observer) => { - observer.error("should not have invoked observable"); - }); - }), - ]); - ApolloLink.execute(chain, uniqueOperation); - } - ); + it("should pass operation from one link to next with modifications", async () => { + const chain = ApolloLink.from([ + new ApolloLink((op, forward) => + forward({ + ...op, + query: sampleQuery, + }) + ), + new ApolloLink((op) => { + expect({ + extensions: {}, + operationName: "SampleQuery", + query: sampleQuery, + variables: {}, + }).toEqual(op); + + return new Observable((observer) => { + observer.complete(); + }); + }), + ]); + const observable = ApolloLink.execute(chain, uniqueOperation); + const stream = new ObservableStream(observable); - itAsync( - "should pass result of one link to another with forward", - (resolve, reject) => { - const data: FetchResult = { - data: { - hello: "world", - }, - }; + await expect(stream).toComplete(); + }); + + it("should pass result of one link to another with forward", async () => { + const data: FetchResult = { + data: { + hello: "world", + }, + }; + + const chain = ApolloLink.from([ + new ApolloLink((op, forward) => { + return forward(op); + }), + new ApolloLink(() => Observable.of(data)), + ]); + const observable = ApolloLink.execute(chain, uniqueOperation); + const stream = new ObservableStream(observable); - const chain = ApolloLink.from([ - new ApolloLink((op, forward) => { - const observable = forward(op); + await expect(stream).toEmitValue(data); + await expect(stream).toComplete(); + }); + it("should receive final result of two link chain", async () => { + const data: FetchResult = { + data: { + hello: "world", + }, + }; + + const chain = ApolloLink.from([ + new ApolloLink((op, forward) => { + const observable = forward(op); + + return new Observable((observer) => { observable.subscribe({ next: (actualData) => { expect(data).toEqual(actualData); + observer.next({ + data: { + ...actualData.data, + modification: "unique", + }, + }); }, - error: () => { - throw new Error(); - }, - complete: resolve, + error: (error) => observer.error(error), + complete: () => observer.complete(), }); + }); + }), + new ApolloLink(() => Observable.of(data)), + ]); - return observable; - }), - new ApolloLink(() => Observable.of(data)), - ]); - ApolloLink.execute(chain, uniqueOperation); - } - ); + const result = ApolloLink.execute(chain, uniqueOperation); + const stream = new ObservableStream(result); - itAsync( - "should receive final result of two link chain", - (resolve, reject) => { - const data: FetchResult = { - data: { - hello: "world", - }, - }; + await expect(stream).toEmitValue({ + data: { + ...data.data, + modification: "unique", + }, + }); + await expect(stream).toComplete(); + }); - const chain = ApolloLink.from([ - new ApolloLink((op, forward) => { - const observable = forward(op); - - return new Observable((observer) => { - observable.subscribe({ - next: (actualData) => { - expect(data).toEqual(actualData); - observer.next({ - data: { - ...actualData.data, - modification: "unique", - }, - }); - }, - error: (error) => observer.error(error), - complete: () => observer.complete(), - }); - }); - }), - new ApolloLink(() => Observable.of(data)), - ]); + it("should chain together a function with links", async () => { + const add1 = new ApolloLink((operation: Operation, forward: NextLink) => { + operation.setContext((context: { num: number }) => ({ + num: context.num + 1, + })); + return forward(operation); + }); + const add1Link = new ApolloLink((operation, forward) => { + operation.setContext((context: { num: number }) => ({ + num: context.num + 1, + })); + return forward(operation); + }); - const result = ApolloLink.execute(chain, uniqueOperation); + const link = ApolloLink.from([ + add1, + add1, + add1Link, + add1, + add1Link, + new ApolloLink((operation) => + Observable.of({ data: operation.getContext() }) + ), + ]); - result.subscribe({ - next: (modifiedData) => { - expect({ - data: { - ...data.data, - modification: "unique", - }, - }).toEqual(modifiedData); - }, - error: () => { - throw new Error(); - }, - complete: resolve, - }); - } - ); - - itAsync( - "should chain together a function with links", - (resolve, reject) => { - const add1 = new ApolloLink( - (operation: Operation, forward: NextLink) => { - operation.setContext((context: { num: number }) => ({ - num: context.num + 1, - })); - return forward(operation); - } - ); - const add1Link = new ApolloLink((operation, forward) => { - operation.setContext((context: { num: number }) => ({ - num: context.num + 1, - })); - return forward(operation); - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context: { num: 0 } }) + ); - const link = ApolloLink.from([ - add1, - add1, - add1Link, - add1, - add1Link, - new ApolloLink((operation) => - Observable.of({ data: operation.getContext() }) - ), - ]); - testLinkResults({ - link, - results: [{ num: 5 }], - context: { num: 0 }, - done: resolve, - }); - } - ); + await expect(stream).toEmitValue({ data: { num: 5 } }); + await expect(stream).toComplete(); + }); }); describe("split", () => { - itAsync("should split two functions", (resolve, reject) => { + it("should split two functions", async () => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat((operation, forward) => @@ -670,21 +599,28 @@ describe("ApolloClient", () => { link2 ); - testLinkResults({ - link, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.add = 2; - testLinkResults({ - link, - results: [4], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 4 }); + await expect(stream).toComplete(); + } }); - itAsync("should split two Links", (resolve, reject) => { + it("should split two Links", async () => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat( @@ -703,21 +639,28 @@ describe("ApolloClient", () => { link2 ); - testLinkResults({ - link, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.add = 2; - testLinkResults({ - link, - results: [4], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 4 }); + await expect(stream).toComplete(); + } }); - itAsync("should split a link and a function", (resolve, reject) => { + it("should split a link and a function", async () => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat((operation, forward) => @@ -734,21 +677,28 @@ describe("ApolloClient", () => { link2 ); - testLinkResults({ - link, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.add = 2; - testLinkResults({ - link, - results: [4], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 4 }); + await expect(stream).toComplete(); + } }); - itAsync("should allow concat after split to be join", (resolve, reject) => { + it("should allow concat after split to be join", async () => { const context = { test: true, add: 1 }; const start = new SetContextLink(() => ({ ...context })); const link = start @@ -771,92 +721,105 @@ describe("ApolloClient", () => { Observable.of({ data: operation.getContext().add }) ); - testLinkResults({ - link, - context, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - context, - results: [3], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); + } }); - itAsync( - "should allow default right to be empty or passthrough when forward available", - (resolve, reject) => { - let context = { test: true }; - const start = new SetContextLink(() => context); - const link = start.split( - (operation) => operation.getContext().test, - (operation) => - Observable.of({ - data: { - count: 1, - }, - }) - ); - const concat = link.concat((operation) => + it("should allow default right to be empty or passthrough when forward available", async () => { + let context = { test: true }; + const start = new SetContextLink(() => context); + const link = start.split( + (operation) => operation.getContext().test, + (operation) => Observable.of({ data: { - count: 2, + count: 1, }, }) + ); + const concat = link.concat((operation) => + Observable.of({ + data: { + count: 2, + }, + }) + ); + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) ); - testLinkResults({ - link, - results: [{ count: 1 }], - }); + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } - context.test = false; + context.test = false; - testLinkResults({ - link, - results: [], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link: concat, - results: [{ count: 2 }], - done: resolve, - }); + await expect(stream).toComplete(); } - ); - itAsync( - "should create filter when single link passed in", - (resolve, reject) => { - const link = ApolloLink.split( - (operation) => operation.getContext().test, - (operation, forward) => Observable.of({ data: { count: 1 } }) + { + const stream = new ObservableStream( + execute(concat, { query: sampleQuery }) ); - let context = { test: true }; + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } + }); - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + it("should create filter when single link passed in", async () => { + const link = ApolloLink.split( + (operation) => operation.getContext().test, + (operation, forward) => Observable.of({ data: { count: 1 } }) + ); - context.test = false; + let context = { test: true }; - testLinkResults({ - link, - results: [], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); } - ); - itAsync("should split two functions", (resolve, reject) => { + context.test = false; + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toComplete(); + } + }); + + it("should split two functions", async () => { const link = ApolloLink.split( (operation) => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), @@ -865,23 +828,28 @@ describe("ApolloClient", () => { let context = { test: true }; - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - results: [{ count: 2 }], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } }); - itAsync("should split two Links", (resolve, reject) => { + it("should split two Links", async () => { const link = ApolloLink.split( (operation) => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), @@ -892,23 +860,28 @@ describe("ApolloClient", () => { let context = { test: true }; - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - results: [{ count: 2 }], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } }); - itAsync("should split a link and a function", (resolve, reject) => { + it("should split a link and a function", async () => { const link = ApolloLink.split( (operation) => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), @@ -919,23 +892,28 @@ describe("ApolloClient", () => { let context = { test: true }; - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - results: [{ count: 2 }], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } }); - itAsync("should allow concat after split to be join", (resolve, reject) => { + it("should allow concat after split to be join", async () => { const context = { test: true }; const link = ApolloLink.split( (operation) => operation.getContext().test, @@ -945,47 +923,53 @@ describe("ApolloClient", () => { })) ).concat(() => Observable.of({ data: { count: 1 } })); - testLinkResults({ - link, - context, - results: [{ count: 2 }], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } context.test = false; + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); - testLinkResults({ - link, - context, - results: [{ count: 1 }], - done: resolve, - }); + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } }); - itAsync( - "should allow default right to be passthrough", - (resolve, reject) => { - const context = { test: true }; - const link = ApolloLink.split( - (operation) => operation.getContext().test, - (operation) => Observable.of({ data: { count: 2 } }) - ).concat((operation) => Observable.of({ data: { count: 1 } })); + it("should allow default right to be passthrough", async () => { + const context = { test: true }; + const link = ApolloLink.split( + (operation) => operation.getContext().test, + (operation) => Observable.of({ data: { count: 2 } }) + ).concat((operation) => Observable.of({ data: { count: 1 } })); - testLinkResults({ - link, - context, - results: [{ count: 2 }], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); - context.test = false; + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } - testLinkResults({ - link, - context, - results: [{ count: 1 }], - done: resolve, - }); + context.test = false; + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); } - ); + }); }); describe("Terminating links", () => { diff --git a/src/link/http/__tests__/parseAndCheckHttpResponse.ts b/src/link/http/__tests__/parseAndCheckHttpResponse.ts index 74e9b63018e..b667d95c35b 100644 --- a/src/link/http/__tests__/parseAndCheckHttpResponse.ts +++ b/src/link/http/__tests__/parseAndCheckHttpResponse.ts @@ -3,7 +3,6 @@ import fetchMock from "fetch-mock"; import { createOperation } from "../../utils/createOperation"; import { parseAndCheckHttpResponse } from "../parseAndCheckHttpResponse"; -import { itAsync } from "../../../testing"; const query = gql` query SampleQuery { @@ -20,98 +19,79 @@ describe("parseAndCheckResponse", () => { const operations = [createOperation({}, { query })]; - itAsync( - "throws a Server error when response is > 300 with unparsable json", - (resolve, reject) => { - const status = 400; - fetchMock.mock("begin:/error", status); - fetch("error") - .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(status); - expect(e.name).toBe("ServerError"); - expect(e).toHaveProperty("response"); - expect(e.bodyText).toBe(undefined); - resolve(); - }) - .catch(reject); - } - ); - - itAsync( - "throws a ServerParse error when response is 200 with unparsable json", - (resolve, reject) => { - const status = 200; - fetchMock.mock("begin:/error", status); - fetch("error") - .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(status); - expect(e.name).toBe("ServerParseError"); - expect(e).toHaveProperty("response"); - expect(e).toHaveProperty("bodyText"); - resolve(); - }) - .catch(reject); - } - ); - - itAsync( - "throws a network error with a status code and result", - (resolve, reject) => { - const status = 403; - const body = { data: "fail" }; //does not contain data or errors - fetchMock.mock("begin:/error", { - body, - status, - }); - fetch("error") - .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(status); - expect(e.name).toBe("ServerError"); - expect(e).toHaveProperty("response"); - expect(e).toHaveProperty("result"); - resolve(); - }) - .catch(reject); - } - ); + it("throws a Server error when response is > 300 with unparsable json", async () => { + const status = 400; + fetchMock.mock("begin:/error", status); + + const error = await fetch("error") + .then(parseAndCheckHttpResponse(operations)) + .catch((error) => error); + + expect(error.statusCode).toBe(status); + expect(error.name).toBe("ServerError"); + expect(error).toHaveProperty("response"); + expect(error.bodyText).toBe(undefined); + }); + + it("throws a ServerParse error when response is 200 with unparsable json", async () => { + const status = 200; + fetchMock.mock("begin:/error", status); + const error = await fetch("error") + .then(parseAndCheckHttpResponse(operations)) + .catch((error) => error); + + expect(error.statusCode).toBe(status); + expect(error.name).toBe("ServerParseError"); + expect(error).toHaveProperty("response"); + expect(error).toHaveProperty("bodyText"); + }); - itAsync("throws a server error on incorrect data", (resolve, reject) => { + it("throws a network error with a status code and result", async () => { + const status = 403; + const body = { data: "fail" }; //does not contain data or errors + fetchMock.mock("begin:/error", { + body, + status, + }); + const error = await fetch("error") + .then(parseAndCheckHttpResponse(operations)) + .catch((error) => error); + + expect(error.statusCode).toBe(status); + expect(error.name).toBe("ServerError"); + expect(error).toHaveProperty("response"); + expect(error).toHaveProperty("result"); + }); + + it("throws a server error on incorrect data", async () => { const data = { hello: "world" }; //does not contain data or erros fetchMock.mock("begin:/incorrect", data); - fetch("incorrect") + const error = await fetch("incorrect") .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(200); - expect(e.name).toBe("ServerError"); - expect(e).toHaveProperty("response"); - expect(e.result).toEqual(data); - resolve(); - }) - .catch(reject); + .catch((error) => error); + + expect(error.statusCode).toBe(200); + expect(error.name).toBe("ServerError"); + expect(error).toHaveProperty("response"); + expect(error.result).toEqual(data); }); - itAsync("is able to return a correct GraphQL result", (resolve, reject) => { + it("is able to return a correct GraphQL result", async () => { const errors = ["", "" + new Error("hi")]; const data = { data: { hello: "world" }, errors }; fetchMock.mock("begin:/data", { body: data, }); - fetch("data") - .then(parseAndCheckHttpResponse(operations)) - .then(({ data, errors: e }) => { - expect(data).toEqual({ hello: "world" }); - expect(e.length).toEqual(errors.length); - expect(e).toEqual(errors); - resolve(); - }) - .catch(reject); + + { + const { data, errors: e } = await fetch("data").then( + parseAndCheckHttpResponse(operations) + ); + + expect(data).toEqual({ hello: "world" }); + expect(e.length).toEqual(errors.length); + expect(e).toEqual(errors); + } }); }); diff --git a/src/testing/core/observableToPromise.ts b/src/testing/core/observableToPromise.ts deleted file mode 100644 index 428517e1aff..00000000000 --- a/src/testing/core/observableToPromise.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { ObservableQuery, ApolloQueryResult } from "../../core/index.js"; -import type { ObservableSubscription } from "../../utilities/index.js"; - -export interface Options { - /** - * The ObservableQuery to subscribe to. - */ - observable: ObservableQuery; - /** - * Should we resolve after seeing all our callbacks? [default: true] - * (use this if you are racing the promise against another) - */ - shouldResolve?: boolean; - /** - * How long to wait after seeing desired callbacks before resolving? - * [default: -1 => don't wait] - */ - wait?: number; - /** - * An expected set of errors. - */ - errorCallbacks?: ((error: Error) => any)[]; -} - -export type ResultCallback = (result: ApolloQueryResult) => any; - -// Take an observable and N callbacks, and observe the observable, -// ensuring it is called exactly N times, resolving once it has done so. -// Optionally takes a timeout, which it will wait X ms after the Nth callback -// to ensure it is not called again. -export function observableToPromiseAndSubscription( - { observable, shouldResolve = true, wait = -1, errorCallbacks = [] }: Options, - ...cbs: ResultCallback[] -): { promise: Promise; subscription: ObservableSubscription } { - let subscription: ObservableSubscription = null as never; - const promise = new Promise((resolve, reject) => { - let errorIndex = 0; - let cbIndex = 0; - const results: any[] = []; - - const tryToResolve = () => { - if (!shouldResolve) { - return; - } - - const done = () => { - subscription.unsubscribe(); - // XXX: we could pass a few other things out here? - resolve(results); - }; - - if (cbIndex === cbs.length && errorIndex === errorCallbacks.length) { - if (wait === -1) { - done(); - } else { - setTimeout(done, wait); - } - } - }; - - let queue = Promise.resolve(); - - subscription = observable.subscribe({ - next(result: ApolloQueryResult) { - queue = queue - .then(() => { - const cb = cbs[cbIndex++]; - if (cb) return cb(result); - reject( - new Error( - `Observable 'next' method called more than ${cbs.length} times` - ) - ); - }) - .then((res) => { - results.push(res); - tryToResolve(); - }, reject); - }, - error(error: Error) { - queue = queue - .then(() => { - const errorCb = errorCallbacks[errorIndex++]; - if (errorCb) return errorCb(error); - reject(error); - }) - .then(tryToResolve, reject); - }, - }); - }); - - return { - promise, - subscription, - }; -} - -export default function ( - options: Options, - ...cbs: ResultCallback[] -): Promise { - return observableToPromiseAndSubscription(options, ...cbs).promise; -} diff --git a/src/testing/react/__tests__/MockedProvider.test.tsx b/src/testing/react/__tests__/MockedProvider.test.tsx index f4a5caed150..8751429cce9 100644 --- a/src/testing/react/__tests__/MockedProvider.test.tsx +++ b/src/testing/react/__tests__/MockedProvider.test.tsx @@ -3,7 +3,7 @@ import { DocumentNode } from "graphql"; import { act, render, screen, waitFor } from "@testing-library/react"; import gql from "graphql-tag"; -import { itAsync, MockedResponse, MockLink } from "../../core"; +import { MockedResponse, MockLink } from "../../core"; import { MockedProvider } from "../MockedProvider"; import { useQuery } from "../../../react/hooks"; import { InMemoryCache } from "../../../cache"; @@ -82,7 +82,7 @@ describe("General use", () => { errorThrown = false; }); - itAsync("should mock the data", (resolve, reject) => { + it("should mock the data", async () => { let finished = false; function Component({ username }: Variables) { const { loading, data } = useQuery(query, { variables }); @@ -99,106 +99,97 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync( - "should pass the variables to the result function", - async (resolve, reject) => { - function Component({ ...variables }: Variables) { - useQuery(query, { variables }); - return null; - } + it("should pass the variables to the result function", async () => { + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; + } - const mock2: MockedResponse = { - request: { - query, - variables, - }, - result: jest.fn().mockResolvedValue({ data: { user } }), - }; + const mock2: MockedResponse = { + request: { + query, + variables, + }, + result: jest.fn().mockResolvedValue({ data: { user } }), + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(mock2.result as jest.Mock).toHaveBeenCalledWith(variables); - }).then(resolve, reject); + await waitFor(() => { + expect(mock2.result as jest.Mock).toHaveBeenCalledWith(variables); + }); + }); + + it("should pass the variables to the variableMatcher", async () => { + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; } - ); - - itAsync( - "should pass the variables to the variableMatcher", - async (resolve, reject) => { - function Component({ ...variables }: Variables) { - useQuery(query, { variables }); - return null; - } - const mock2: MockedResponse = { - request: { - query, - }, - variableMatcher: jest.fn().mockReturnValue(true), - result: { data: { user } }, - }; + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: jest.fn().mockReturnValue(true), + result: { data: { user } }, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(mock2.variableMatcher as jest.Mock).toHaveBeenCalledWith( - variables - ); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(mock2.variableMatcher as jest.Mock).toHaveBeenCalledWith( + variables + ); + }); + }); - itAsync( - "should use a mock if the variableMatcher returns true", - async (resolve, reject) => { - let finished = false; + it("should use a mock if the variableMatcher returns true", async () => { + let finished = false; - function Component({ username }: Variables) { - const { loading, data } = useQuery(query, { - variables, - }); - if (!loading) { - expect(data!.user).toMatchSnapshot(); - finished = true; - } - return null; + function Component({ username }: Variables) { + const { loading, data } = useQuery(query, { + variables, + }); + if (!loading) { + expect(data!.user).toMatchSnapshot(); + finished = true; } + return null; + } - const mock2: MockedResponse = { - request: { - query, - }, - variableMatcher: (v) => v.username === variables.username, - result: { data: { user } }, - }; + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: (v) => v.username === variables.username, + result: { data: { user } }, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); - itAsync("should allow querying with the typename", (resolve, reject) => { + it("should allow querying with the typename", async () => { let finished = false; function Component({ username }: Variables) { const { loading, data } = useQuery(query, { variables }); @@ -225,12 +216,12 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync("should allow using a custom cache", (resolve, reject) => { + it("should allow using a custom cache", async () => { let finished = false; const cache = new InMemoryCache(); cache.writeQuery({ @@ -254,169 +245,157 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync( - "should error if the variables in the mock and component do not match", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + it("should error if the variables in the mock and component do not match", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const variables2 = { - username: "other_user", - age: undefined, - }; + const variables2 = { + username: "other_user", + age: undefined, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should error if the variableMatcher returns false", - async (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should error if the variableMatcher returns false", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const mock2: MockedResponse = { - request: { - query, - }, - variableMatcher: () => false, - result: { data: { user } }, - }; + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: () => false, + result: { data: { user } }, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should error if the variables do not deep equal", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should error if the variables do not deep equal", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const mocks2 = [ - { - request: { - query, - variables: { - age: 13, - username: "some_user", - }, + const mocks2 = [ + { + request: { + query, + variables: { + age: 13, + username: "some_user", }, - result: { data: { user } }, }, - ]; + result: { data: { user } }, + }, + ]; - const variables2 = { - username: "some_user", - age: 42, - }; + const variables2 = { + username: "some_user", + age: 42, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should not error if the variables match but have different order", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, data } = useQuery(query, { - variables, - }); - if (!loading) { - expect(data).toMatchSnapshot(); - finished = true; - } - return null; + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should not error if the variables match but have different order", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, data } = useQuery(query, { + variables, + }); + if (!loading) { + expect(data).toMatchSnapshot(); + finished = true; } + return null; + } - const mocks2 = [ - { - request: { - query, - variables: { - age: 13, - username: "some_user", - }, + const mocks2 = [ + { + request: { + query, + variables: { + age: 13, + username: "some_user", }, - result: { data: { user } }, }, - ]; + result: { data: { user } }, + }, + ]; - const variables2 = { - username: "some_user", - age: 13, - }; + const variables2 = { + username: "some_user", + age: 13, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); - itAsync("should support mocking a network error", (resolve, reject) => { + it("should support mocking a network error", async () => { let finished = false; function Component({ ...variables }: Variables) { const { loading, error } = useQuery(query, { @@ -447,53 +426,50 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync( - "should error if the query in the mock and component do not match", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + it("should error if the query in the mock and component do not match", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const mocksDifferentQuery = [ - { - request: { - query: gql` - query OtherQuery { - otherQuery { - id - } + const mocksDifferentQuery = [ + { + request: { + query: gql` + query OtherQuery { + otherQuery { + id } - `, - variables, - }, - result: { data: { user } }, + } + `, + variables, }, - ]; + result: { data: { user } }, + }, + ]; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); it("should pass down props prop in mock as props for the component", () => { function Component({ ...variables }) { @@ -523,71 +499,68 @@ describe("General use", () => { unmount(); }); - itAsync( - "should support returning mocked results from a function", - (resolve, reject) => { - let finished = false; - let resultReturned = false; + it("should support returning mocked results from a function", async () => { + let finished = false; + let resultReturned = false; - const testUser = { - __typename: "User", - id: 12345, - }; + const testUser = { + __typename: "User", + id: 12345, + }; - function Component({ ...variables }: Variables) { - const { loading, data } = useQuery(query, { - variables, - }); - if (!loading) { - expect(data!.user).toEqual(testUser); - expect(resultReturned).toBe(true); - finished = true; - } - return null; + function Component({ ...variables }: Variables) { + const { loading, data } = useQuery(query, { + variables, + }); + if (!loading) { + expect(data!.user).toEqual(testUser); + expect(resultReturned).toBe(true); + finished = true; } + return null; + } - const testQuery: DocumentNode = gql` - query GetUser($username: String!) { - user(username: $username) { - id - } + const testQuery: DocumentNode = gql` + query GetUser($username: String!) { + user(username: $username) { + id } - `; + } + `; - const testVariables = { - username: "jsmith", - }; - const testMocks = [ - { - request: { - query: testQuery, - variables: testVariables, - }, - result() { - resultReturned = true; - return { - data: { - user: { - __typename: "User", - id: 12345, - }, + const testVariables = { + username: "jsmith", + }; + const testMocks = [ + { + request: { + query: testQuery, + variables: testVariables, + }, + result() { + resultReturned = true; + return { + data: { + user: { + __typename: "User", + id: 12345, }, - }; - }, + }, + }; }, - ]; + }, + ]; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); it('should return "No more mocked responses" errors in response', async () => { let finished = false; @@ -1028,66 +1001,60 @@ describe("General use", () => { consoleSpy.mockRestore(); }); - itAsync( - "should support custom error handling using setOnError", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - useQuery(query, { variables }); - return null; - } + it("should support custom error handling using setOnError", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; + } - const mockLink = new MockLink([], true, { showWarnings: false }); - mockLink.setOnError((error) => { - expect(error).toMatchSnapshot(); - finished = true; - }); - const link = ApolloLink.from([errorLink, mockLink]); + const mockLink = new MockLink([], true, { showWarnings: false }); + mockLink.setOnError((error) => { + expect(error).toMatchSnapshot(); + finished = true; + }); + const link = ApolloLink.from([errorLink, mockLink]); - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should pipe exceptions thrown in custom onError functions through the link chain", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; - } + await waitFor(() => { + expect(finished).toBe(true); + }); + }); - const mockLink = new MockLink([], true, { showWarnings: false }); - mockLink.setOnError(() => { - throw new Error("oh no!"); + it("should pipe exceptions thrown in custom onError functions through the link chain", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, }); - const link = ApolloLink.from([errorLink, mockLink]); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; + } + return null; + } - render( - - - - ); + const mockLink = new MockLink([], true, { showWarnings: false }); + mockLink.setOnError(() => { + throw new Error("oh no!"); + }); + const link = ApolloLink.from([errorLink, mockLink]); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + render( + + + + ); + + await waitFor(() => { + expect(finished).toBe(true); + }); + }); it("should support loading state testing with delay", async () => { jest.useFakeTimers(); @@ -1224,100 +1191,94 @@ describe("General use", () => { }); describe("@client testing", () => { - itAsync( - "should support @client fields with a custom cache", - (resolve, reject) => { - let finished = false; - const cache = new InMemoryCache(); - - cache.writeQuery({ - query: gql` - { - networkStatus { - isOnline - } + it("should support @client fields with a custom cache", async () => { + let finished = false; + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: gql` + { + networkStatus { + isOnline } - `, - data: { - networkStatus: { - __typename: "NetworkStatus", - isOnline: true, - }, + } + `, + data: { + networkStatus: { + __typename: "NetworkStatus", + isOnline: true, }, - }); + }, + }); - function Component() { - const { loading, data } = useQuery(gql` - { - networkStatus @client { - isOnline - } + function Component() { + const { loading, data } = useQuery(gql` + { + networkStatus @client { + isOnline } - `); - if (!loading) { - expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); - expect(data!.networkStatus.isOnline).toEqual(true); - finished = true; } - return null; + `); + if (!loading) { + expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); + expect(data!.networkStatus.isOnline).toEqual(true); + finished = true; } + return null; + } - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should support @client fields with field policies", - (resolve, reject) => { - let finished = false; - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - networkStatus() { - return { - __typename: "NetworkStatus", - isOnline: true, - }; - }, + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should support @client fields with field policies", async () => { + let finished = false; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + networkStatus() { + return { + __typename: "NetworkStatus", + isOnline: true, + }; }, }, }, - }); + }, + }); - function Component() { - const { loading, data } = useQuery(gql` - { - networkStatus @client { - isOnline - } + function Component() { + const { loading, data } = useQuery(gql` + { + networkStatus @client { + isOnline } - `); - if (!loading) { - expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); - expect(data!.networkStatus.isOnline).toEqual(true); - finished = true; } - return null; + `); + if (!loading) { + expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); + expect(data!.networkStatus.isOnline).toEqual(true); + finished = true; } + return null; + } - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); });