From ead88848c5839a6a4008152b1964523ffdd66b82 Mon Sep 17 00:00:00 2001
From: Alessia Bellisario <github@bellisar.io>
Date: Fri, 14 Jun 2024 11:09:07 -0400
Subject: [PATCH 1/3] wip

---
 .storybook/stories/ApolloComponent.stories.ts | 14 ++--
 .storybook/stories/Welcome.mdx                | 69 +++++++++++++++++-
 .../apollo-client/ApolloComponent.tsx         | 70 ++++++++++++++++---
 README.md                                     | 32 ++++-----
 src/handlers.ts                               |  7 +-
 5 files changed, 154 insertions(+), 38 deletions(-)

diff --git a/.storybook/stories/ApolloComponent.stories.ts b/.storybook/stories/ApolloComponent.stories.ts
index 449381c..330092b 100644
--- a/.storybook/stories/ApolloComponent.stories.ts
+++ b/.storybook/stories/ApolloComponent.stories.ts
@@ -1,14 +1,18 @@
 import type { Meta, StoryObj } from "@storybook/react";
 import { within, expect, waitFor } from "@storybook/test";
-import ApolloComponent from "./components/apollo-client/ApolloComponent.js";
+import {
+  ApolloApp,
+  ApolloAppWithDefer,
+} from "./components/apollo-client/ApolloComponent.js";
 import { createHandler } from "../../src/handlers";
 import { schemaWithMocks } from "../../src/__tests__/mocks/handlers";
+import { Canvas } from "@storybook/blocks";
 
 const { handler } = createHandler(schemaWithMocks);
 
 const meta = {
   title: "Example/ApolloComponent",
-  component: ApolloComponent,
+  component: ApolloApp,
   parameters: {
     layout: "centered",
     msw: {
@@ -17,13 +21,15 @@ const meta = {
       },
     },
   },
-} satisfies Meta<typeof ApolloComponent>;
+} satisfies Meta<typeof ApolloApp>;
 
 export default meta;
 
+export { ApolloAppWithDefer };
+
 type Story = StoryObj<typeof meta>;
 
-export const Primary: Story = {
+export const ApolloAppWithoutDefer: Story = {
   play: async ({ canvasElement }) => {
     const canvas = within(canvasElement);
     await expect(
diff --git a/.storybook/stories/Welcome.mdx b/.storybook/stories/Welcome.mdx
index f04305e..466d68b 100644
--- a/.storybook/stories/Welcome.mdx
+++ b/.storybook/stories/Welcome.mdx
@@ -2,8 +2,71 @@ import { Meta } from "@storybook/blocks";
 
 <Meta title="GraphQL Testing Library" />
 
-# GraphQL Testing Library
+<div align="center">
+  <h1>GraphQL Testing Library</h1>
 
-GraphQL Testing Library provides generic utilities for creating [Mock Service Worker](https://mswjs.io/) handlers for any GraphQL API. MSW is also the [Testing Library-recommended](https://testing-library.com/docs/react-testing-library/example-intro/#full-example) way to declaratively mock API communication in your tests without stubbing `window.fetch`.
+  {/* <a href="https://www.apollographql.com/"><img src="https://raw.githubusercontent.com/apollographql/apollo-client-devtools/main/assets/apollo-wordmark.svg" height="100" alt="Apollo Client" /></a> */}
 
-> This project is not affiliated with the ["Testing Library"](https://github.com/testing-library) ecosystem that this project is clearly inspired from.
+  <p>Testing utilities that encourage good practices for apps built with GraphQL.</p>
+
+</div>
+<hr />
+
+**GraphQL Testing Library** provides utilities that make it easy to generate [Mock Service Worker](https://mswjs.io/) handlers for any GraphQL API.
+
+MSW is the [Testing Library-recommended](https://testing-library.com/docs/react-testing-library/example-intro/#full-example) way to declaratively mock API communication in your tests without stubbing `window.fetch`.
+
+This library currently supports incremental delivery features `@defer` and `@stream` out of the box, with plans to add support for subscriptions over multipart HTTP and possibly WebSockets which are [currently in beta in MSW](https://github.com/mswjs/msw/discussions/2010).
+
+> This project is not affiliated with the ["Testing Library"](https://github.com/testing-library) ecosystem that inspired it. We're just fans :)
+
+
+## Installation
+
+This library has `peerDependencies` listings for `msw` at `^2.0.0` and `graphql` at `^15.0.0 || ^16.0.0`. Install them along with this library using your preferred package manager:
+
+```
+npm install --save-dev @apollo/graphql-testing-library msw graphql
+pnpm add --save-dev @apollo/graphql-testing-library msw graphql
+yarn add --dev @apollo/graphql-testing-library msw graphql
+bun add --dev @apollo/graphql-testing-library msw graphql
+```
+
+## Usage
+
+### `createHandler`
+
+```typescript
+import { createHandler } from "@apollo/graphql-testing-library";
+import { addMocksToSchema } from "@graphql-tools/mock";
+import { makeExecutableSchema } from "@graphql-tools/schema";
+import typeDefs from "./schema.graphql";
+
+// Create an executable schema
+const schema = makeExecutableSchema({ typeDefs });
+
+// Add mock resolvers
+const schemaWithMocks = addMocksToSchema({
+  schema,
+  resolvers: {
+    Query: {
+      products: () =>
+        Array.from({ length: 5 }, (_element, id) => ({
+          id: `product-${id}`,
+        })),
+    },
+  },
+});
+
+// `createHandler` returns an object with `handler` and `replaceSchema`
+// functions: `handler` is a MSW handler that will intercept all GraphQL
+// operations, and `replaceSchema` allows you to replace the mock schema
+// the `handler` use to resolve requests against.
+const { handler, replaceSchema } = createHandler(schemaWithMocks, {
+  // It accepts a config object as the second argument where you can specify a 
+  // delay min and max, which will add random delays to your tests within the /
+  // threshold to simulate a real network connection.
+  // Default: delay: { min: 300, max: 300 }
+  delay: { min: 200, max: 500 },
+});
+```
diff --git a/.storybook/stories/components/apollo-client/ApolloComponent.tsx b/.storybook/stories/components/apollo-client/ApolloComponent.tsx
index 3644071..a93821d 100644
--- a/.storybook/stories/components/apollo-client/ApolloComponent.tsx
+++ b/.storybook/stories/components/apollo-client/ApolloComponent.tsx
@@ -1,4 +1,4 @@
-import { Suspense } from "react";
+import { Suspense, type ReactNode } from "react";
 import type { TypedDocumentNode } from "@apollo/client";
 import {
   gql,
@@ -9,8 +9,8 @@ import {
   HttpLink,
   useSuspenseQuery,
 } from "@apollo/client";
-import { Container } from "../Container.js";
 import { Product, Reviews } from "../Product.js";
+import { Container } from "../Container.js";
 
 const httpLink = new HttpLink({
   uri: "https://main--hack-the-e-commerce.apollographos.net/graphql",
@@ -25,7 +25,7 @@ export const makeClient = () =>
 
 export const client = makeClient();
 
-const QUERY: TypedDocumentNode<{
+const APP_QUERY: TypedDocumentNode<{
   products: {
     id: string;
     title: string;
@@ -33,6 +33,30 @@ const QUERY: TypedDocumentNode<{
     description: string;
     reviews: Array<{ rating: number; id: string; content: string }>;
   }[];
+}> = gql`
+  query AppQuery {
+    products {
+      id
+      reviews {
+        id
+        rating
+        content
+      }
+      title
+      mediaUrl
+      description
+    }
+  }
+`;
+
+const APP_QUERY_WITH_DEFER: TypedDocumentNode<{
+  products: {
+    id: string;
+    title: string;
+    mediaUrl: string;
+    description: string;
+    reviews?: Array<{ rating: number; id: string; content: string }>;
+  }[];
 }> = gql`
   query AppQuery {
     products {
@@ -51,18 +75,32 @@ const QUERY: TypedDocumentNode<{
   }
 `;
 
-export default function App() {
+export function Wrapper({ children }: { children: ReactNode }) {
   return (
     <ApolloProvider client={client}>
-      <Suspense fallback={<h1>Loading...</h1>}>
-        <Main />
-      </Suspense>
+      <Suspense fallback={<h1>Loading...</h1>}>{children}</Suspense>
     </ApolloProvider>
   );
 }
 
-export function Main() {
-  const { data } = useSuspenseQuery(QUERY);
+export function ApolloApp() {
+  return (
+    <Wrapper>
+      <App />
+    </Wrapper>
+  );
+}
+
+export function ApolloAppWithDefer() {
+  return (
+    <Wrapper>
+      <AppWithDefer />
+    </Wrapper>
+  );
+}
+
+export function App() {
+  const { data } = useSuspenseQuery(APP_QUERY);
 
   return (
     <Container>
@@ -74,3 +112,17 @@ export function Main() {
     </Container>
   );
 }
+
+export function AppWithDefer() {
+  const { data } = useSuspenseQuery(APP_QUERY_WITH_DEFER);
+
+  return (
+    <Container>
+      {data.products.map((product) => (
+        <Product key={product.id} product={product}>
+          <Reviews reviews={product.reviews || []} />
+        </Product>
+      ))}
+    </Container>
+  );
+}
diff --git a/README.md b/README.md
index 8a2280e..9414468 100644
--- a/README.md
+++ b/README.md
@@ -19,23 +19,13 @@ This library currently supports incremental delivery features `@defer` and `@str
 
 ## Installation
 
-```
-npm install --save-dev @apollo/graphql-testing-library
-```
-
-or for installation via yarn
+This library has `peerDependencies` listings for `msw` at `^2.0.0` and `graphql` at `^15.0.0 || ^16.0.0`. Install them along with this library using your preferred package manager:
 
 ```
-yarn add --dev @apollo/graphql-testing-library
-```
-
-This library has `peerDependencies` listings for `msw` at `^2.0.0` and `graphql` at `^15.0.0 || ^16.0.0`.
-
-```
-npm install --save-dev msw graphql
-
-
-yarn add --dev msw graphql
+npm install --save-dev @apollo/graphql-testing-library msw graphql
+pnpm add --save-dev @apollo/graphql-testing-library msw graphql
+yarn add --dev @apollo/graphql-testing-library msw graphql
+bun add --dev @apollo/graphql-testing-library msw graphql
 ```
 
 ## Usage
@@ -64,11 +54,15 @@ const schemaWithMocks = addMocksToSchema({
   },
 });
 
-// createHandler returns an object with `handler` and `replaceSchema`
-// functions: `handler` is your MSW handler, and `replaceSchema` can
-// be used in tests to pass a new `schemaWithMocks` that your `handler`
-// will use to resolve requests against
+// `createHandler` returns an object with `handler` and `replaceSchema`
+// functions: `handler` is a MSW handler that will intercept all GraphQL
+// operations, and `replaceSchema` allows you to replace the mock schema
+// the `handler` use to resolve requests against.
 const { handler, replaceSchema } = createHandler(schemaWithMocks, {
+  // It accepts a config object as the second argument where you can specify a 
+  // delay min and max, which will add random delays to your tests within the /
+  // threshold to simulate a real network connection.
+  // Default: delay: { min: 300, max: 300 }
   delay: { min: 200, max: 500 },
 });
 ```
diff --git a/src/handlers.ts b/src/handlers.ts
index 3bd7a4c..5f393b7 100644
--- a/src/handlers.ts
+++ b/src/handlers.ts
@@ -131,15 +131,16 @@ export const createHandler = (
         const stream = new ReadableStream({
           async start(controller) {
             try {
-              for (const c of chunks) {
+              for (const chunk of chunks) {
                 if (delayMin > 0) {
                   const randomDelay =
                     Math.random() * (delayMax - delayMin) + delayMin;
-                  if (c === boundary) {
+
+                  if (chunk === boundary || chunk === terminatingBoundary) {
                     await wait(randomDelay);
                   }
                 }
-                controller.enqueue(encoder.encode(c));
+                controller.enqueue(encoder.encode(chunk));
               }
             } finally {
               controller.close();

From 63990a2bc610f777624fcf428faab35a64ab4c34 Mon Sep 17 00:00:00 2001
From: Alessia Bellisario <github@bellisar.io>
Date: Fri, 14 Jun 2024 12:27:43 -0400
Subject: [PATCH 2/3] Update README and stories

---
 .storybook/stories/ApolloComponent.stories.ts |   8 +-
 .storybook/stories/RelayComponent.stories.ts  |  15 +-
 .../apollo-client/ApolloComponent.tsx         |   2 +-
 .../components/relay/RelayComponent.tsx       | 180 +++++-------------
 .../RelayComponentAppQuery.graphql.ts         |  62 +++---
 ...RelayComponentWithDeferAppQuery.graphql.ts | 162 ++++++++++++++++
 .../components/relay/relay-environment.ts     | 112 +++++++++++
 src/__tests__/handlers.test.tsx               |   6 +-
 src/handlers.ts                               |   3 +-
 9 files changed, 369 insertions(+), 181 deletions(-)
 create mode 100644 .storybook/stories/components/relay/__generated__/RelayComponentWithDeferAppQuery.graphql.ts
 create mode 100644 .storybook/stories/components/relay/relay-environment.ts

diff --git a/.storybook/stories/ApolloComponent.stories.ts b/.storybook/stories/ApolloComponent.stories.ts
index 330092b..4f7fdaf 100644
--- a/.storybook/stories/ApolloComponent.stories.ts
+++ b/.storybook/stories/ApolloComponent.stories.ts
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
 import { within, expect, waitFor } from "@storybook/test";
 import {
   ApolloApp,
-  ApolloAppWithDefer,
+  ApolloAppWithDefer as AppWithDefer,
 } from "./components/apollo-client/ApolloComponent.js";
 import { createHandler } from "../../src/handlers";
 import { schemaWithMocks } from "../../src/__tests__/mocks/handlers";
@@ -11,7 +11,7 @@ import { Canvas } from "@storybook/blocks";
 const { handler } = createHandler(schemaWithMocks);
 
 const meta = {
-  title: "Example/ApolloComponent",
+  title: "Example/Apollo",
   component: ApolloApp,
   parameters: {
     layout: "centered",
@@ -25,11 +25,11 @@ const meta = {
 
 export default meta;
 
-export { ApolloAppWithDefer };
+export { AppWithDefer };
 
 type Story = StoryObj<typeof meta>;
 
-export const ApolloAppWithoutDefer: Story = {
+export const App: Story = {
   play: async ({ canvasElement }) => {
     const canvas = within(canvasElement);
     await expect(
diff --git a/.storybook/stories/RelayComponent.stories.ts b/.storybook/stories/RelayComponent.stories.ts
index 638d0f9..b8d4f44 100644
--- a/.storybook/stories/RelayComponent.stories.ts
+++ b/.storybook/stories/RelayComponent.stories.ts
@@ -1,14 +1,17 @@
 import type { Meta, StoryObj } from "@storybook/react";
 import { within, expect, waitFor } from "@storybook/test";
-import RelayComponent from "./components/relay/RelayComponent.js";
+import {
+  RelayApp,
+  RelayAppWithDefer as AppWithDefer,
+} from "./components/relay/RelayComponent.js";
 import { createHandler } from "../../src/handlers";
 import { schemaWithMocks } from "../../src/__tests__/mocks/handlers";
 
 const { handler } = createHandler(schemaWithMocks);
 
 const meta = {
-  title: "Example/RelayComponent",
-  component: RelayComponent,
+  title: "Example/Relay",
+  component: RelayApp,
   parameters: {
     layout: "centered",
     msw: {
@@ -17,13 +20,15 @@ const meta = {
       },
     },
   },
-} satisfies Meta<typeof RelayComponent>;
+} satisfies Meta<typeof RelayApp>;
 
 export default meta;
 
+export { AppWithDefer };
+
 type Story = StoryObj<typeof meta>;
 
-export const Primary: Story = {
+export const App: Story = {
   play: async ({ canvasElement }) => {
     const canvas = within(canvasElement);
     await expect(
diff --git a/.storybook/stories/components/apollo-client/ApolloComponent.tsx b/.storybook/stories/components/apollo-client/ApolloComponent.tsx
index a93821d..480643c 100644
--- a/.storybook/stories/components/apollo-client/ApolloComponent.tsx
+++ b/.storybook/stories/components/apollo-client/ApolloComponent.tsx
@@ -75,7 +75,7 @@ const APP_QUERY_WITH_DEFER: TypedDocumentNode<{
   }
 `;
 
-export function Wrapper({ children }: { children: ReactNode }) {
+function Wrapper({ children }: { children: ReactNode }) {
   return (
     <ApolloProvider client={client}>
       <Suspense fallback={<h1>Loading...</h1>}>{children}</Suspense>
diff --git a/.storybook/stories/components/relay/RelayComponent.tsx b/.storybook/stories/components/relay/RelayComponent.tsx
index 4327457..8921196 100644
--- a/.storybook/stories/components/relay/RelayComponent.tsx
+++ b/.storybook/stories/components/relay/RelayComponent.tsx
@@ -1,134 +1,19 @@
-import { Suspense } from "react";
+import { Suspense, type ReactNode } from "react";
 import type { RelayComponentAppQuery } from "./__generated__/RelayComponentAppQuery.graphql.js";
 import {
   RelayEnvironmentProvider,
-  loadQuery,
   useFragment,
-  usePreloadedQuery,
+  useLazyLoadQuery,
 } from "react-relay";
-import { serializeFetchParameter } from "@apollo/client";
-import type { CacheConfig, RequestParameters } from "relay-runtime";
-import {
-  Environment,
-  Network,
-  Observable,
-  RecordSource,
-  Store,
-  graphql,
-  QueryResponseCache,
-} from "relay-runtime";
-import type { Variables } from "relay-runtime";
-
-import { maybe } from "@apollo/client/utilities";
-import {
-  handleError,
-  readMultipartBody,
-} from "@apollo/client/link/http/parseAndCheckHttpResponse";
+import { graphql } from "relay-runtime";
 import { Container } from "../Container.js";
 import { Product, Reviews as ReviewsContainer } from "../Product.js";
+import { RelayEnvironment } from "./relay-environment.js";
 
-const uri = "https://main--hack-the-e-commerce.apollographos.net/graphql";
-
-const oneMinute = 60 * 1000;
-const cache = new QueryResponseCache({ size: 250, ttl: oneMinute });
-
-const backupFetch = maybe(() => fetch);
-
-function fetchQuery(
-  operation: RequestParameters,
-  variables: Variables,
-  cacheConfig: CacheConfig
-) {
-  const queryID = operation.text;
-  const isMutation = operation.operationKind === "mutation";
-  const isQuery = operation.operationKind === "query";
-  const forceFetch = cacheConfig && cacheConfig.force;
-
-  // Try to get data from cache on queries
-  const fromCache = cache.get(queryID, variables);
-  if (isQuery && fromCache !== null && !forceFetch) {
-    return fromCache;
-  }
-
-  const body = {
-    operationName: operation.name,
-    variables,
-    query: operation.text || "",
-  };
-
-  const options: {
-    method: string;
-    headers: Record<string, any>;
-    body?: string;
-  } = {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/json",
-      accept: "multipart/mixed;deferSpec=20220824,application/json",
-    },
-  };
-
-  return Observable.create((sink) => {
-    try {
-      options.body = serializeFetchParameter(body, "Payload");
-    } catch (parseError) {
-      sink.error(parseError as Error);
-    }
-
-    const currentFetch = maybe(() => fetch) || backupFetch;
-
-    const observerNext = (data) => {
-      if ("incremental" in data) {
-        for (const item of data.incremental) {
-          sink.next(item);
-        }
-      } else if ("data" in data) {
-        sink.next(data);
-      }
-    };
-
-    currentFetch!(uri, options)
-      .then(async (response) => {
-        const ctype = response.headers?.get("content-type");
-
-        if (ctype !== null && /^multipart\/mixed/i.test(ctype)) {
-          return readMultipartBody(response, observerNext);
-        } else {
-          const json = await response.json();
-
-          if (isQuery && json) {
-            cache.set(queryID, variables, json);
-          }
-          // Clear cache on mutations
-          if (isMutation) {
-            cache.clear();
-          }
-
-          observerNext(json);
-        }
-      })
-      .then(() => {
-        sink.complete();
-      })
-      .catch((err: any) => {
-        handleError(err, sink);
-      });
-  });
-}
-
-const network = Network.create(fetchQuery);
-
-export const RelayEnvironment = new Environment({
-  network,
-  store: new Store(new RecordSource()),
-});
-
-export default function App() {
+export function Wrapper({ children }: { children: ReactNode }) {
   return (
     <RelayEnvironmentProvider environment={RelayEnvironment}>
-      <Suspense fallback={<h1>Loading...</h1>}>
-        <Main />
-      </Suspense>
+      <Suspense fallback={<h1>Loading...</h1>}>{children}</Suspense>
     </RelayEnvironmentProvider>
   );
 }
@@ -145,6 +30,18 @@ const ratingsFragment = graphql`
 
 const appQuery = graphql`
   query RelayComponentAppQuery {
+    products {
+      id
+      ...RelayComponentReviewsFragment_product
+      title
+      mediaUrl
+      description
+    }
+  }
+`;
+
+const appQueryWithDefer = graphql`
+  query RelayComponentWithDeferAppQuery {
     products {
       id
       ...RelayComponentReviewsFragment_product @defer
@@ -155,17 +52,40 @@ const appQuery = graphql`
   }
 `;
 
-const queryReference = loadQuery<RelayComponentAppQuery>(
-  RelayEnvironment,
-  appQuery,
-  {}
-);
+export function RelayApp() {
+  return (
+    <Wrapper>
+      <App />
+    </Wrapper>
+  );
+}
 
-function Main() {
-  const data = usePreloadedQuery<RelayComponentAppQuery>(
-    appQuery,
-    queryReference
+export function RelayAppWithDefer() {
+  return (
+    <Wrapper>
+      <AppWithDefer />
+    </Wrapper>
   );
+}
+
+function App() {
+  const data = useLazyLoadQuery<RelayComponentAppQuery>(appQuery, {});
+
+  return (
+    <Container>
+      {data?.products?.map((product) => (
+        <Product key={product.id} product={product}>
+          <Suspense fallback="-">
+            <Reviews key={product.id} product={product} />
+          </Suspense>
+        </Product>
+      ))}
+    </Container>
+  );
+}
+
+function AppWithDefer() {
+  const data = useLazyLoadQuery<RelayComponentAppQuery>(appQueryWithDefer, {});
 
   return (
     <Container>
diff --git a/.storybook/stories/components/relay/__generated__/RelayComponentAppQuery.graphql.ts b/.storybook/stories/components/relay/__generated__/RelayComponentAppQuery.graphql.ts
index f73e0ec..8655db4 100644
--- a/.storybook/stories/components/relay/__generated__/RelayComponentAppQuery.graphql.ts
+++ b/.storybook/stories/components/relay/__generated__/RelayComponentAppQuery.graphql.ts
@@ -1,5 +1,5 @@
 /**
- * @generated SignedSource<<c3853a3faff397fafe6ad0095371cbdc>>
+ * @generated SignedSource<<a85c38cc7d612228276089944f119b72>>
  * @lightSyntaxTransform
  * @nogrep
  */
@@ -71,14 +71,9 @@ return {
         "selections": [
           (v0/*: any*/),
           {
-            "kind": "Defer",
-            "selections": [
-              {
-                "args": null,
-                "kind": "FragmentSpread",
-                "name": "RelayComponentReviewsFragment_product"
-              }
-            ]
+            "args": null,
+            "kind": "FragmentSpread",
+            "name": "RelayComponentReviewsFragment_product"
           },
           (v1/*: any*/),
           (v2/*: any*/),
@@ -106,37 +101,30 @@ return {
         "selections": [
           (v0/*: any*/),
           {
-            "if": null,
-            "kind": "Defer",
-            "label": "RelayComponentAppQuery$defer$RelayComponentReviewsFragment_product",
+            "alias": null,
+            "args": null,
+            "concreteType": "Review",
+            "kind": "LinkedField",
+            "name": "reviews",
+            "plural": true,
             "selections": [
+              (v0/*: any*/),
+              {
+                "alias": null,
+                "args": null,
+                "kind": "ScalarField",
+                "name": "rating",
+                "storageKey": null
+              },
               {
                 "alias": null,
                 "args": null,
-                "concreteType": "Review",
-                "kind": "LinkedField",
-                "name": "reviews",
-                "plural": true,
-                "selections": [
-                  (v0/*: any*/),
-                  {
-                    "alias": null,
-                    "args": null,
-                    "kind": "ScalarField",
-                    "name": "rating",
-                    "storageKey": null
-                  },
-                  {
-                    "alias": null,
-                    "args": null,
-                    "kind": "ScalarField",
-                    "name": "content",
-                    "storageKey": null
-                  }
-                ],
+                "kind": "ScalarField",
+                "name": "content",
                 "storageKey": null
               }
-            ]
+            ],
+            "storageKey": null
           },
           (v1/*: any*/),
           (v2/*: any*/),
@@ -147,16 +135,16 @@ return {
     ]
   },
   "params": {
-    "cacheID": "d8a71936337a80402d5cddfb525ec135",
+    "cacheID": "3b9c3b8cd3c59679be38e6d0f8dfc557",
     "id": null,
     "metadata": {},
     "name": "RelayComponentAppQuery",
     "operationKind": "query",
-    "text": "query RelayComponentAppQuery {\n  products {\n    id\n    ...RelayComponentReviewsFragment_product @defer(label: \"RelayComponentAppQuery$defer$RelayComponentReviewsFragment_product\")\n    title\n    mediaUrl\n    description\n  }\n}\n\nfragment RelayComponentReviewsFragment_product on Product {\n  reviews {\n    id\n    rating\n    content\n  }\n}\n"
+    "text": "query RelayComponentAppQuery {\n  products {\n    id\n    ...RelayComponentReviewsFragment_product\n    title\n    mediaUrl\n    description\n  }\n}\n\nfragment RelayComponentReviewsFragment_product on Product {\n  reviews {\n    id\n    rating\n    content\n  }\n}\n"
   }
 };
 })();
 
-(node as any).hash = "34db6443c113b6471fea59a54e8029e9";
+(node as any).hash = "fe50ea1f5ee94d4806b295f629910cd4";
 
 export default node;
diff --git a/.storybook/stories/components/relay/__generated__/RelayComponentWithDeferAppQuery.graphql.ts b/.storybook/stories/components/relay/__generated__/RelayComponentWithDeferAppQuery.graphql.ts
new file mode 100644
index 0000000..1efce97
--- /dev/null
+++ b/.storybook/stories/components/relay/__generated__/RelayComponentWithDeferAppQuery.graphql.ts
@@ -0,0 +1,162 @@
+/**
+ * @generated SignedSource<<67e68fd02a277ad442924bac7db281d6>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ConcreteRequest, Query } from 'relay-runtime';
+import { FragmentRefs } from "relay-runtime";
+export type RelayComponentWithDeferAppQuery$variables = Record<PropertyKey, never>;
+export type RelayComponentWithDeferAppQuery$data = {
+  readonly products: ReadonlyArray<{
+    readonly description: string | null | undefined;
+    readonly id: string;
+    readonly mediaUrl: string | null | undefined;
+    readonly title: string | null | undefined;
+    readonly " $fragmentSpreads": FragmentRefs<"RelayComponentReviewsFragment_product">;
+  } | null | undefined> | null | undefined;
+};
+export type RelayComponentWithDeferAppQuery = {
+  response: RelayComponentWithDeferAppQuery$data;
+  variables: RelayComponentWithDeferAppQuery$variables;
+};
+
+const node: ConcreteRequest = (function(){
+var v0 = {
+  "alias": null,
+  "args": null,
+  "kind": "ScalarField",
+  "name": "id",
+  "storageKey": null
+},
+v1 = {
+  "alias": null,
+  "args": null,
+  "kind": "ScalarField",
+  "name": "title",
+  "storageKey": null
+},
+v2 = {
+  "alias": null,
+  "args": null,
+  "kind": "ScalarField",
+  "name": "mediaUrl",
+  "storageKey": null
+},
+v3 = {
+  "alias": null,
+  "args": null,
+  "kind": "ScalarField",
+  "name": "description",
+  "storageKey": null
+};
+return {
+  "fragment": {
+    "argumentDefinitions": [],
+    "kind": "Fragment",
+    "metadata": null,
+    "name": "RelayComponentWithDeferAppQuery",
+    "selections": [
+      {
+        "alias": null,
+        "args": null,
+        "concreteType": "Product",
+        "kind": "LinkedField",
+        "name": "products",
+        "plural": true,
+        "selections": [
+          (v0/*: any*/),
+          {
+            "kind": "Defer",
+            "selections": [
+              {
+                "args": null,
+                "kind": "FragmentSpread",
+                "name": "RelayComponentReviewsFragment_product"
+              }
+            ]
+          },
+          (v1/*: any*/),
+          (v2/*: any*/),
+          (v3/*: any*/)
+        ],
+        "storageKey": null
+      }
+    ],
+    "type": "Query",
+    "abstractKey": null
+  },
+  "kind": "Request",
+  "operation": {
+    "argumentDefinitions": [],
+    "kind": "Operation",
+    "name": "RelayComponentWithDeferAppQuery",
+    "selections": [
+      {
+        "alias": null,
+        "args": null,
+        "concreteType": "Product",
+        "kind": "LinkedField",
+        "name": "products",
+        "plural": true,
+        "selections": [
+          (v0/*: any*/),
+          {
+            "if": null,
+            "kind": "Defer",
+            "label": "RelayComponentWithDeferAppQuery$defer$RelayComponentReviewsFragment_product",
+            "selections": [
+              {
+                "alias": null,
+                "args": null,
+                "concreteType": "Review",
+                "kind": "LinkedField",
+                "name": "reviews",
+                "plural": true,
+                "selections": [
+                  (v0/*: any*/),
+                  {
+                    "alias": null,
+                    "args": null,
+                    "kind": "ScalarField",
+                    "name": "rating",
+                    "storageKey": null
+                  },
+                  {
+                    "alias": null,
+                    "args": null,
+                    "kind": "ScalarField",
+                    "name": "content",
+                    "storageKey": null
+                  }
+                ],
+                "storageKey": null
+              }
+            ]
+          },
+          (v1/*: any*/),
+          (v2/*: any*/),
+          (v3/*: any*/)
+        ],
+        "storageKey": null
+      }
+    ]
+  },
+  "params": {
+    "cacheID": "5d0a1e385798573ffda670a713f233e4",
+    "id": null,
+    "metadata": {},
+    "name": "RelayComponentWithDeferAppQuery",
+    "operationKind": "query",
+    "text": "query RelayComponentWithDeferAppQuery {\n  products {\n    id\n    ...RelayComponentReviewsFragment_product @defer(label: \"RelayComponentWithDeferAppQuery$defer$RelayComponentReviewsFragment_product\")\n    title\n    mediaUrl\n    description\n  }\n}\n\nfragment RelayComponentReviewsFragment_product on Product {\n  reviews {\n    id\n    rating\n    content\n  }\n}\n"
+  }
+};
+})();
+
+(node as any).hash = "191fcdc886b10cd48ed92df8b781b5c0";
+
+export default node;
diff --git a/.storybook/stories/components/relay/relay-environment.ts b/.storybook/stories/components/relay/relay-environment.ts
new file mode 100644
index 0000000..aeacd29
--- /dev/null
+++ b/.storybook/stories/components/relay/relay-environment.ts
@@ -0,0 +1,112 @@
+import { serializeFetchParameter } from "@apollo/client";
+import type { CacheConfig, RequestParameters } from "relay-runtime";
+import {
+  Environment,
+  Network,
+  Observable,
+  RecordSource,
+  Store,
+  QueryResponseCache,
+} from "relay-runtime";
+import type { Variables } from "relay-runtime";
+import { maybe } from "@apollo/client/utilities";
+import {
+  handleError,
+  readMultipartBody,
+} from "@apollo/client/link/http/parseAndCheckHttpResponse";
+
+const uri = "https://main--hack-the-e-commerce.apollographos.net/graphql";
+
+const oneMinute = 60 * 1000;
+const cache = new QueryResponseCache({ size: 250, ttl: oneMinute });
+
+const backupFetch = maybe(() => fetch);
+
+function fetchQuery(
+  operation: RequestParameters,
+  variables: Variables,
+  cacheConfig: CacheConfig
+) {
+  const queryID = operation.text;
+  const isMutation = operation.operationKind === "mutation";
+  const isQuery = operation.operationKind === "query";
+  const forceFetch = cacheConfig && cacheConfig.force;
+
+  // Try to get data from cache on queries
+  const fromCache = cache.get(queryID, variables);
+  if (isQuery && fromCache !== null && !forceFetch) {
+    return fromCache;
+  }
+
+  const body = {
+    operationName: operation.name,
+    variables,
+    query: operation.text || "",
+  };
+
+  const options: {
+    method: string;
+    headers: Record<string, any>;
+    body?: string;
+  } = {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+      accept: "multipart/mixed;deferSpec=20220824,application/json",
+    },
+  };
+
+  return Observable.create((sink) => {
+    try {
+      options.body = serializeFetchParameter(body, "Payload");
+    } catch (parseError) {
+      sink.error(parseError as Error);
+    }
+
+    const currentFetch = maybe(() => fetch) || backupFetch;
+
+    const observerNext = (data) => {
+      if ("incremental" in data) {
+        for (const item of data.incremental) {
+          sink.next(item);
+        }
+      } else if ("data" in data) {
+        sink.next(data);
+      }
+    };
+
+    currentFetch!(uri, options)
+      .then(async (response) => {
+        const ctype = response.headers?.get("content-type");
+
+        if (ctype !== null && /^multipart\/mixed/i.test(ctype)) {
+          return readMultipartBody(response, observerNext);
+        } else {
+          const json = await response.json();
+
+          if (isQuery && json) {
+            cache.set(queryID, variables, json);
+          }
+          // Clear cache on mutations
+          if (isMutation) {
+            cache.clear();
+          }
+
+          observerNext(json);
+        }
+      })
+      .then(() => {
+        sink.complete();
+      })
+      .catch((err: any) => {
+        handleError(err, sink);
+      });
+  });
+}
+
+const network = Network.create(fetchQuery);
+
+export const RelayEnvironment = new Environment({
+  network,
+  store: new Store(new RecordSource()),
+});
\ No newline at end of file
diff --git a/src/__tests__/handlers.test.tsx b/src/__tests__/handlers.test.tsx
index 837f046..36fa285 100644
--- a/src/__tests__/handlers.test.tsx
+++ b/src/__tests__/handlers.test.tsx
@@ -1,7 +1,7 @@
 import { ApolloProvider } from "@apollo/client";
 import { Suspense } from "react";
 import {
-  Main,
+  AppWithDefer,
   makeClient,
 } from "../../.storybook/stories/components/apollo-client/ApolloComponent.tsx";
 import { addMocksToSchema } from "@graphql-tools/mock";
@@ -18,7 +18,7 @@ describe("integration tests", () => {
     render(
       <ApolloProvider client={client}>
         <Suspense fallback={<h1>Loading...</h1>}>
-          <Main />
+          <AppWithDefer />
         </Suspense>
       </ApolloProvider>
     );
@@ -73,7 +73,7 @@ describe("integration tests", () => {
     render(
       <ApolloProvider client={client}>
         <Suspense fallback={<h1>Loading...</h1>}>
-          <Main />
+          <AppWithDefer />
         </Suspense>
       </ApolloProvider>
     );
diff --git a/src/handlers.ts b/src/handlers.ts
index 5f393b7..1c3eba5 100644
--- a/src/handlers.ts
+++ b/src/handlers.ts
@@ -160,7 +160,8 @@ export const createHandler = (
           schema: testSchema,
           variableValues: variables,
         });
-
+        const randomDelay = Math.random() * (delayMax - delayMin) + delayMin;
+        await wait(randomDelay);
         return HttpResponse.json(result as SingularExecutionResult<any, any>);
       }
     }),

From 4b443a7389987109216133a3c80e366394cad7c1 Mon Sep 17 00:00:00 2001
From: Alessia Bellisario <github@bellisar.io>
Date: Fri, 14 Jun 2024 12:47:48 -0400
Subject: [PATCH 3/3] Update package

---
 .github/workflows/release.yml                 | 20 +++++++++----------
 .storybook/stories/Welcome.mdx                |  6 +++++-
 .../apollo-client/ApolloComponent.tsx         |  4 ++++
 .../components/relay/RelayComponent.tsx       |  4 ++++
 README.md                                     |  6 +++++-
 package.json                                  |  7 ++++---
 6 files changed, 32 insertions(+), 15 deletions(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5dc9b78..4f8fe3a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -54,13 +54,13 @@ jobs:
           version: 9
           run_install: true
 
-      # - name: Create release PR or publish to npm + GitHub
-      #   id: changesets
-      #   if: steps.check_files.outputs.files_exists == 'false'
-      #   uses: changesets/action@v1
-      #   with:
-      #     version: pnpm run changeset-version
-      #     publish: pnpm run changeset-publish
-      #   env:
-      #     GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      #     NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+      - name: Create release PR or publish to npm + GitHub
+        id: changesets
+        if: steps.check_files.outputs.files_exists == 'false'
+        uses: changesets/action@v1
+        with:
+          version: pnpm run changeset-version
+          publish: pnpm run changeset-publish
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/.storybook/stories/Welcome.mdx b/.storybook/stories/Welcome.mdx
index 466d68b..3977568 100644
--- a/.storybook/stories/Welcome.mdx
+++ b/.storybook/stories/Welcome.mdx
@@ -16,7 +16,7 @@ import { Meta } from "@storybook/blocks";
 
 MSW is the [Testing Library-recommended](https://testing-library.com/docs/react-testing-library/example-intro/#full-example) way to declaratively mock API communication in your tests without stubbing `window.fetch`.
 
-This library currently supports incremental delivery features `@defer` and `@stream` out of the box, with plans to add support for subscriptions over multipart HTTP and possibly WebSockets which are [currently in beta in MSW](https://github.com/mswjs/msw/discussions/2010).
+This library currently supports incremental delivery features `@defer` and `@stream` out of the box, with plans to support subscriptions over multipart HTTP as well as other transports such as WebSockets, [currently in beta in MSW](https://github.com/mswjs/msw/discussions/2010).
 
 > This project is not affiliated with the ["Testing Library"](https://github.com/testing-library) ecosystem that inspired it. We're just fans :)
 
@@ -38,6 +38,10 @@ bun add --dev @apollo/graphql-testing-library msw graphql
 
 ```typescript
 import { createHandler } from "@apollo/graphql-testing-library";
+
+// We suggest using @graphql-tools/mock and @graphql-tools/schema
+// to create a schema with mock resolvers.
+// See https://the-guild.dev/graphql/tools/docs/mocking for more info.
 import { addMocksToSchema } from "@graphql-tools/mock";
 import { makeExecutableSchema } from "@graphql-tools/schema";
 import typeDefs from "./schema.graphql";
diff --git a/.storybook/stories/components/apollo-client/ApolloComponent.tsx b/.storybook/stories/components/apollo-client/ApolloComponent.tsx
index 480643c..c414727 100644
--- a/.storybook/stories/components/apollo-client/ApolloComponent.tsx
+++ b/.storybook/stories/components/apollo-client/ApolloComponent.tsx
@@ -100,6 +100,8 @@ export function ApolloAppWithDefer() {
 }
 
 export function App() {
+  // Use useSuspenseQuery here because we want to demo the loading experience
+  // with/without defer.
   const { data } = useSuspenseQuery(APP_QUERY);
 
   return (
@@ -114,6 +116,8 @@ export function App() {
 }
 
 export function AppWithDefer() {
+  // Use useSuspenseQuery here because we want to demo the loading experience
+  // with/without defer.
   const { data } = useSuspenseQuery(APP_QUERY_WITH_DEFER);
 
   return (
diff --git a/.storybook/stories/components/relay/RelayComponent.tsx b/.storybook/stories/components/relay/RelayComponent.tsx
index 8921196..a54f5b2 100644
--- a/.storybook/stories/components/relay/RelayComponent.tsx
+++ b/.storybook/stories/components/relay/RelayComponent.tsx
@@ -69,6 +69,8 @@ export function RelayAppWithDefer() {
 }
 
 function App() {
+  // Use useLazyLoadQuery here because we want to demo the loading experience
+  // with/without defer.
   const data = useLazyLoadQuery<RelayComponentAppQuery>(appQuery, {});
 
   return (
@@ -85,6 +87,8 @@ function App() {
 }
 
 function AppWithDefer() {
+  // Use useLazyLoadQuery here because we want to demo the loading experience
+  // with/without defer.
   const data = useLazyLoadQuery<RelayComponentAppQuery>(appQueryWithDefer, {});
 
   return (
diff --git a/README.md b/README.md
index 9414468..f986a0f 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
 
 MSW is the [Testing Library-recommended](https://testing-library.com/docs/react-testing-library/example-intro/#full-example) way to declaratively mock API communication in your tests without stubbing `window.fetch`.
 
-This library currently supports incremental delivery features `@defer` and `@stream` out of the box, with plans to add support for subscriptions over multipart HTTP and possibly WebSockets which are [currently in beta in MSW](https://github.com/mswjs/msw/discussions/2010).
+This library currently supports incremental delivery features `@defer` and `@stream` out of the box, with plans to support subscriptions over multipart HTTP as well as other transports such as WebSockets, [currently in beta in MSW](https://github.com/mswjs/msw/discussions/2010).
 
 > This project is not affiliated with the ["Testing Library"](https://github.com/testing-library) ecosystem that inspired it. We're just fans :)
 
@@ -34,6 +34,10 @@ bun add --dev @apollo/graphql-testing-library msw graphql
 
 ```typescript
 import { createHandler } from "@apollo/graphql-testing-library";
+
+// We suggest using @graphql-tools/mock and @graphql-tools/schema
+// to create a schema with mock resolvers.
+// See https://the-guild.dev/graphql/tools/docs/mocking for more info.
 import { addMocksToSchema } from "@graphql-tools/mock";
 import { makeExecutableSchema } from "@graphql-tools/schema";
 import typeDefs from "./schema.graphql";
diff --git a/package.json b/package.json
index f31afde..3fc0b9d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@apollo/graphql-testing-library",
-  "version": "0.1.0",
+  "version": "0.0.0",
   "private": true,
   "repository": {
     "url": "git+https://github.com/apollographql/graphql-testing-library"
@@ -33,10 +33,11 @@
   "types": "./dist/index.d.ts",
   "scripts": {
     "build": "tsup",
+    "clean": "rm -rf dist",
     "prepdist:changesets": "node scripts/prepareDist.cjs",
-    "changeset-publish": "npm run build && npm run prepdist:changesets && cd dist && changeset publish",
-    "changeset-check": "changeset status --verbose --since=origin/main",
+    "changeset-publish": "pnpm run clean && pnpm run build && pnpm run prepdist:changesets && cd dist && changeset publish",
     "changeset-version": "changeset version && pnpm i",
+    "changeset-check": "changeset status --verbose --since=origin/main",
     "test": "jest",
     "relay": "relay-compiler",
     "lint": "eslint --ext .ts src",