From c9767130a8232a8771fc71a3c56196509c01be5c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 11 Dec 2024 16:16:37 -0700 Subject: [PATCH] Remove itAsync from optimistic tests --- src/__tests__/optimistic.ts | 1875 ++++++++++++++++------------------- 1 file changed, 832 insertions(+), 1043 deletions(-) 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(); }); });