diff --git a/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.test.tsx b/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.test.tsx index ea4c3373..1551a6f0 100644 --- a/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.test.tsx +++ b/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.test.tsx @@ -1,17 +1,23 @@ -import React, { Suspense, useMemo } from "react"; -import { runInConditions, testIn } from "../util/runInConditions.js"; +import React, { Suspense, useMemo } from "rehackt"; +import { outsideOf } from "../util/runInConditions.js"; +import assert from "node:assert"; +import test, { afterEach, describe } from "node:test"; +import type { + QueryEvent, + TransportIdentifier, +} from "./DataTransportAbstraction.js"; + import type { TypedDocumentNode, WatchQueryOptions, } from "@apollo/client/index.js"; -import { gql } from "@apollo/client/index.js"; - -import "global-jsdom/register"; -import assert from "node:assert"; -import { afterEach } from "node:test"; -import type { QueryEvent } from "./DataTransportAbstraction.js"; - -runInConditions("browser", "node"); +import { MockSubscriptionLink } from "@apollo/client/testing/core/mocking/mockSubscriptionLink.js"; +import { + useSuspenseQuery, + gql, + DocumentTransform, +} from "@apollo/client/index.js"; +import { visit, Kind, print, isDefinitionNode } from "graphql"; const { ApolloClient, @@ -19,196 +25,321 @@ const { WrapApolloProvider, DataTransportContext, } = await import("#bundled"); -const { useSuspenseQuery } = await import("@apollo/client/index.js"); -const { MockSubscriptionLink } = await import( - "@apollo/client/testing/index.js" -); -const { render, cleanup } = await import("@testing-library/react"); -afterEach(cleanup); - -const QUERY_ME: TypedDocumentNode<{ me: string }> = gql` - query { - me - } -`; -const FIRST_REQUEST: WatchQueryOptions = { - fetchPolicy: "cache-first", - nextFetchPolicy: undefined, - notifyOnNetworkStatusChange: false, - query: QUERY_ME, -}; -const EVENT_STARTED: QueryEvent = { - type: "started", - id: "1" as any, - options: FIRST_REQUEST, -}; -const FIRST_RESULT = { me: "User" }; -const EVENT_DATA: QueryEvent = { - type: "data", - id: "1" as any, - result: { data: FIRST_RESULT }, -}; -const EVENT_COMPLETE: QueryEvent = { - type: "complete", - id: "1" as any, -}; -const FIRST_HOOK_RESULT = { - data: FIRST_RESULT, - networkStatus: 7, -}; - -await testIn("node")( - "`useSuspenseQuery`: data is getting sent to the transport", +await describe( + "tests with DOM access", + { skip: outsideOf("node", "browser") }, async () => { - const events: QueryEvent[] = []; - const staticData: unknown[] = []; + // @ts-expect-error seems to have a wrong type? + await import("global-jsdom/register"); + const { render, cleanup } = await import("@testing-library/react"); - function useStaticValueRef(current: T) { - staticData.push(current); - return { current }; - } + afterEach(cleanup); - const Provider = WrapApolloProvider( - ({ children, registerDispatchRequestStarted }) => { - registerDispatchRequestStarted!(({ event, observable }) => { - events.push(event); - observable.subscribe({ - next: events.push.bind(events), - }); - }); - return ( - ({ - useStaticValueRef, - }), - [] - )} - > - {children} - - ); + const QUERY_ME: TypedDocumentNode<{ me: string }> = gql` + query { + me } - ); + `; + const FIRST_REQUEST: WatchQueryOptions = { + fetchPolicy: "cache-first", + nextFetchPolicy: undefined, + notifyOnNetworkStatusChange: false, + query: QUERY_ME, + }; + const EVENT_STARTED: QueryEvent = { + type: "started", + id: "1" as any, + options: FIRST_REQUEST, + }; + const FIRST_RESULT = { me: "User" }; + const EVENT_DATA: QueryEvent = { + type: "data", + id: "1" as any, + result: { data: FIRST_RESULT }, + }; + const EVENT_COMPLETE: QueryEvent = { + type: "complete", + id: "1" as any, + }; + const FIRST_HOOK_RESULT = { + data: FIRST_RESULT, + networkStatus: 7, + }; - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - connectToDevTools: false, - cache: new InMemoryCache(), - link, - }); + await test( + "`useSuspenseQuery`: data is getting sent to the transport", + { skip: outsideOf("node") }, + async () => { + const events: QueryEvent[] = []; + const staticData: unknown[] = []; - let finishedRenderCount = 0; + function useStaticValueRef(current: T) { + staticData.push(current); + return { current }; + } - function Child() { - const { data } = useSuspenseQuery(QUERY_ME); - finishedRenderCount++; - return <>{data.me}; - } + const Provider = WrapApolloProvider( + ({ children, registerDispatchRequestStarted }) => { + registerDispatchRequestStarted!(({ event, observable }) => { + events.push(event); + observable.subscribe({ + next: events.push.bind(events), + }); + }); + return ( + ({ + useStaticValueRef, + }), + [] + )} + > + {children} + + ); + } + ); - const { findByText } = render( - client}> - - - - - ); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + connectToDevTools: false, + cache: new InMemoryCache(), + link, + }); - assert.deepStrictEqual(events, [EVENT_STARTED]); - assert.deepStrictEqual(staticData, []); + let finishedRenderCount = 0; - link.simulateResult({ result: { data: FIRST_RESULT } }, true); + function Child() { + const { data } = useSuspenseQuery(QUERY_ME); + finishedRenderCount++; + return <>{data.me}; + } - await findByText("User"); + const { findByText } = render( + client}> + + + + + ); - assert.deepStrictEqual(events, [EVENT_STARTED, EVENT_DATA, EVENT_COMPLETE]); - assert.deepStrictEqual( - staticData, - new Array(finishedRenderCount).fill(FIRST_HOOK_RESULT) - ); - } -); + assert.deepStrictEqual(events, [EVENT_STARTED]); + assert.deepStrictEqual(staticData, []); -await testIn("browser")( - "`useSuspenseQuery`: data from the transport is used by the hooks", - async () => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint - let useStaticValueRefStub = (): { current: T } => { - throw new Error("Should not be called yet!"); - }; - let simulateQueryEvent: (event: QueryEvent) => void; - - const Provider = WrapApolloProvider( - ({ children, onQueryEvent, ..._rest }) => { - simulateQueryEvent = onQueryEvent!; - return ( - ({ - useStaticValueRef() { - return useStaticValueRefStub(); - }, - }), - [] - )} - > - {children} - + link.simulateResult({ result: { data: FIRST_RESULT } }, true); + + await findByText("User"); + + assert.deepStrictEqual(events, [ + EVENT_STARTED, + EVENT_DATA, + EVENT_COMPLETE, + ]); + assert.deepStrictEqual( + staticData, + new Array(finishedRenderCount).fill(FIRST_HOOK_RESULT) ); } ); - const client = new ApolloClient({ - connectToDevTools: false, - cache: new InMemoryCache(), - }); + await test( + "`useSuspenseQuery`: data from the transport is used by the hooks", + { skip: outsideOf("browser") }, + async () => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint + let useStaticValueRefStub = (): { current: T } => { + throw new Error("Should not be called yet!"); + }; + let simulateQueryEvent: (event: QueryEvent) => void; - let attemptedRenderCount = 0; - let finishedRenderCount = 0; + const Provider = WrapApolloProvider( + ({ children, onQueryEvent, ..._rest }) => { + simulateQueryEvent = onQueryEvent!; + return ( + ({ + useStaticValueRef() { + return useStaticValueRefStub(); + }, + }), + [] + )} + > + {children} + + ); + } + ); - function Child() { - attemptedRenderCount++; - const { data } = useSuspenseQuery(QUERY_ME); - finishedRenderCount++; - return <>{data.me}; - } + const client = new ApolloClient({ + connectToDevTools: false, + cache: new InMemoryCache(), + }); - const { findByText, rerender } = render( - client}> - ); + let attemptedRenderCount = 0; + let finishedRenderCount = 0; - simulateQueryEvent!(EVENT_STARTED); - rerender( - client}> - - - - - ); + function Child() { + attemptedRenderCount++; + const { data } = useSuspenseQuery(QUERY_ME); + finishedRenderCount++; + return <>{data.me}; + } + + const { findByText, rerender } = render( + client}> + ); - assert.ok(attemptedRenderCount > 0); - assert.ok(finishedRenderCount == 0); - await findByText("Fallback"); + simulateQueryEvent!(EVENT_STARTED); + rerender( + client}> + + + + + ); - useStaticValueRefStub = () => ({ current: FIRST_HOOK_RESULT as any }); - simulateQueryEvent!(EVENT_DATA); - simulateQueryEvent!(EVENT_COMPLETE); + assert.ok(attemptedRenderCount > 0); + assert.ok(finishedRenderCount == 0); + await findByText("Fallback"); - await new Promise((resolve) => setTimeout(resolve, 1000)); + useStaticValueRefStub = () => ({ current: FIRST_HOOK_RESULT as any }); + simulateQueryEvent!(EVENT_DATA); + simulateQueryEvent!(EVENT_COMPLETE); - await findByText("User"); + await new Promise((resolve) => setTimeout(resolve, 1000)); - assert.ok(attemptedRenderCount > 0); - // one render to rehydrate the server value - // one rerender with the actual client value (which is hopefull equal) - assert.equal(finishedRenderCount, 2); + await findByText("User"); - assert.deepStrictEqual(JSON.parse(JSON.stringify(client.extract())), { - ROOT_QUERY: { - __typename: "Query", - me: "User", - }, - }); + assert.ok(attemptedRenderCount > 0); + // one render to rehydrate the server value + // one rerender with the actual client value (which is hopefull equal) + assert.equal(finishedRenderCount, 2); + + assert.deepStrictEqual(JSON.parse(JSON.stringify(client.extract())), { + ROOT_QUERY: { + __typename: "Query", + me: "User", + }, + }); + } + ); } ); + +await describe("document transforms are applied correctly", async () => { + const untransformedQuery = gql` + query Test { + user { + name + } + } + `; + const transformedQuery = gql` + query Test { + user { + name + __typename + id + } + } + `; + const addIdTransform = new DocumentTransform((document) => + visit(document, { + SelectionSet: { + enter(node, _key, parent): undefined | typeof node { + if (isDefinitionNode(parent as any)) return; + return { + ...node, + selections: [ + ...node.selections, + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: "id", + }, + }, + ], + }; + }, + }, + }) + ); + await test("when making a request", async () => { + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + connectToDevTools: false, + documentTransform: addIdTransform, + cache: new InMemoryCache({ + addTypename: true, + }), + link, + }); + const obsQuery = client.watchQuery({ query: untransformedQuery }); + obsQuery.subscribe({}); + await Promise.resolve(); + + assert.equal(print(link.operation!.query), print(transformedQuery)); + }); + + await test( + "when rerunning queries when connection is closed", + { skip: outsideOf("browser") }, + async () => { + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + connectToDevTools: false, + documentTransform: addIdTransform, + cache: new InMemoryCache({ + addTypename: true, + }), + link, + }); + client.onQueryStarted!({ + type: "started", + id: "1" as TransportIdentifier, + options: { + query: untransformedQuery, + }, + }); + client.rerunSimulatedQueries!(); + await Promise.resolve(); + + assert.equal(print(link.operation!.query), print(transformedQuery)); + } + ); + + await test( + "when rerunning a query that failed on the server", + { skip: outsideOf("browser") }, + async () => { + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + connectToDevTools: false, + documentTransform: addIdTransform, + cache: new InMemoryCache({ + addTypename: true, + }), + link, + }); + client.onQueryStarted!({ + type: "started", + id: "1" as TransportIdentifier, + options: { + query: untransformedQuery, + }, + }); + client.onQueryProgress!({ + type: "error", + id: "1" as TransportIdentifier, + }); + await Promise.resolve(); + + assert.equal(print(link.operation!.query), print(transformedQuery)); + } + ); +}); diff --git a/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx b/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx index f7d822ee..a59cc9f9 100644 --- a/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx +++ b/packages/client-react-streaming/src/DataTransportAbstraction/WrappedApolloClient.tsx @@ -300,6 +300,7 @@ export class ApolloClientBrowserImpl< queryManager .fetchQuery(queryId, { ...queryInfo.options, + query: queryManager.transform(queryInfo.options.query), context: { ...queryInfo.options.context, queryDeduplication: false, diff --git a/packages/client-react-streaming/src/importErrors.test.tsx b/packages/client-react-streaming/src/importErrors.test.tsx index 73119287..7db8d95e 100644 --- a/packages/client-react-streaming/src/importErrors.test.tsx +++ b/packages/client-react-streaming/src/importErrors.test.tsx @@ -1,6 +1,6 @@ import assert from "node:assert"; import { test } from "node:test"; -import { testIn } from "./util/runInConditions.js"; +import { outsideOf } from "./util/runInConditions.js"; test("Error message when `WrappedApolloClient` is instantiated with wrong `InMemoryCache`", async () => { const { ApolloClient } = await import("#bundled"); @@ -18,8 +18,9 @@ test("Error message when `WrappedApolloClient` is instantiated with wrong `InMem ); }); -testIn("node")( +test( "Error message when using `ManualDataTransport` with the wrong `ApolloClient`", + { skip: outsideOf("node") }, async () => { const { WrapApolloProvider } = await import("#bundled"); const upstreamPkg = await import("@apollo/client/index.js"); @@ -49,8 +50,9 @@ testIn("node")( } ); -testIn("browser")( +test( "Error message when using `ManualDataTransport` with the wrong `ApolloClient`", + { skip: outsideOf("browser") }, async () => { const { WrapApolloProvider } = await import("#bundled"); const upstreamPkg = await import("@apollo/client/index.js"); diff --git a/packages/client-react-streaming/src/util/runInConditions.ts b/packages/client-react-streaming/src/util/runInConditions.ts index 6af2fec6..450175b3 100644 --- a/packages/client-react-streaming/src/util/runInConditions.ts +++ b/packages/client-react-streaming/src/util/runInConditions.ts @@ -1,5 +1,4 @@ import { parseArgs } from "node:util"; -import { test } from "node:test"; type Condition = "react-server" | "node" | "browser" | "default"; /** @@ -14,16 +13,8 @@ export function runInConditions(...validConditions: Condition[]) { } } -export function testIn(...validConditions: Condition[]) { - if (conditionActive(validConditions)) { - return test; - } - return new Proxy(test, { - apply() {}, - get() { - return function () {}; - }, - }); +export function outsideOf(...validConditions: Condition[]) { + return !conditionActive(validConditions); } function conditionActive(validConditions: Condition[]) {