diff --git a/.changeset/README.md b/.changeset/README.md
new file mode 100644
index 0000000..e5b6d8d
--- /dev/null
+++ b/.changeset/README.md
@@ -0,0 +1,8 @@
+# Changesets
+
+Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
+with multi-package repos, or single-package repos to help you version and publish your code. You can
+find the full documentation for it [in our repository](https://github.com/changesets/changesets)
+
+We have a quick list of common questions to get you started engaging with this project in
+[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
diff --git a/.changeset/config.json b/.changeset/config.json
new file mode 100644
index 0000000..7673ca6
--- /dev/null
+++ b/.changeset/config.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
+ "changelog": [
+ "@changesets/changelog-github",
+ { "repo": "apollographql/graphql-testing-library" }
+ ],
+ "commit": false,
+ "fixed": [],
+ "linked": [],
+ "access": "public",
+ "baseBranch": "main",
+ "updateInternalDependencies": "patch",
+ "ignore": []
+}
diff --git a/.changeset/strong-llamas-wash.md b/.changeset/strong-llamas-wash.md
new file mode 100644
index 0000000..9327808
--- /dev/null
+++ b/.changeset/strong-llamas-wash.md
@@ -0,0 +1,5 @@
+---
+"@apollo/graphql-testing-library": minor
+---
+
+Defer support
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..6a078b9
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,44 @@
+/** @type {import("@types/eslint").Linter.Config} */
+// eslint-disable-next-line no-undef
+module.exports = {
+ env: {
+ browser: true,
+ node: true,
+ es2021: true,
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:storybook/recommended",
+ ],
+ overrides: [],
+ parser: "@typescript-eslint/parser",
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ },
+ plugins: ["@typescript-eslint"],
+ rules: {
+ "@typescript-eslint/no-explicit-any": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ args: "all",
+ argsIgnorePattern: "^_",
+ caughtErrors: "all",
+ caughtErrorsIgnorePattern: "^_",
+ destructuredArrayIgnorePattern: "^_",
+ varsIgnorePattern: "^_",
+ ignoreRestSiblings: true,
+ },
+ ],
+ "@typescript-eslint/consistent-type-imports": [
+ "error",
+ {
+ prefer: "type-imports",
+ disallowTypeAnnotations: false,
+ fixStyle: "separate-type-imports",
+ },
+ ],
+ },
+};
diff --git a/.github/workflows/deploy-storybook.yml b/.github/workflows/deploy-storybook.yml
new file mode 100644
index 0000000..b102c11
--- /dev/null
+++ b/.github/workflows/deploy-storybook.yml
@@ -0,0 +1,42 @@
+name: Deploy Storybook
+
+on:
+ push:
+ branches:
+ - "main"
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ name: Install pnpm
+ with:
+ version: 9
+ run_install: false
+
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Build and publish
+ id: build-publish
+ uses: bitovi/github-actions-storybook-to-github-pages@v1.0.3
+ with:
+ path: storybook-static
+ build_command: pnpm run build-storybook
+ install_command: pnpm i
+ checkout: false
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..4f8fe3a
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,66 @@
+name: Release
+
+on:
+ push:
+ branches:
+ - main
+
+concurrency: ${{ github.workflow }}-${{ github.ref }}
+
+jobs:
+ release:
+ name: Changesets Release
+ # Prevents action from creating a PR on forks
+ if: github.repository == 'apollographql/graphql-testing-library'
+ runs-on: ubuntu-latest
+ # Permissions necessary for Changesets to push a new branch and open PRs
+ # (for automated Version Packages PRs), and request the JWT for provenance.
+ # More info: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings
+ permissions:
+ contents: write
+ pull-requests: write
+ id-token: write
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ # Fetch entire git history so Changesets can generate changelogs
+ # with the correct commits
+ fetch-depth: 0
+
+ - name: Check for pre.json file existence
+ id: check_files
+ uses: andstor/file-existence-action@v3.0.0
+ with:
+ files: ".changeset/pre.json"
+
+ - name: Append NPM token to .npmrc
+ run: |
+ cat << EOF > "$HOME/.npmrc"
+ provenance=true
+ //registry.npmjs.org/:_authToken=$NPM_TOKEN
+ EOF
+ env:
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Setup Node.js 20.x
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.x
+
+ - name: Install pnpm and dependencies
+ uses: pnpm/action-setup@v4
+ with:
+ 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 }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..816bd5e
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,161 @@
+name: Run
+
+on: [push]
+
+concurrency: ${{ github.workflow }}-${{ github.ref }}
+
+jobs:
+ install-and-cache:
+ name: Install and cache
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ name: Install pnpm
+ with:
+ version: 9
+ run_install: false
+
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+
+ - name: Get pnpm store directory
+ shell: bash
+ run: |
+ echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
+
+ - uses: actions/cache@v4
+ name: Setup pnpm cache
+ with:
+ path: ${{ env.STORE_PATH }}
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-pnpm-store-
+
+ - name: Install dependencies
+ run: pnpm install
+
+ test-jest:
+ name: Jest tests
+ if: github.repository == 'apollographql/graphql-testing-library'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9
+ run_install: false
+
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Run Jest tests
+ run: pnpm run test
+
+ test-playwright:
+ name: Playwright tests
+ if: github.repository == 'apollographql/graphql-testing-library'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9
+ run_install: false
+
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Get installed Playwright version
+ id: playwright-version
+ run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package.json').devDependencies['@playwright/test'])")" >> $GITHUB_ENV
+
+ - name: Cache Playwright binaries
+ uses: actions/cache@v3
+ id: playwright-cache
+ with:
+ path: |
+ ~/.cache/ms-playwright
+ key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
+
+ - run: npx playwright install --with-deps
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+
+ - name: Serve Storybook and run tests
+ run: pnpm run build-and-test-storybook
+
+ lint:
+ name: Lint
+ if: github.repository == 'apollographql/graphql-testing-library'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9
+ run_install: false
+
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Lint
+ run: pnpm run lint
+
+ type-check:
+ name: Check types
+ if: github.repository == 'apollographql/graphql-testing-library'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9
+ run_install: false
+
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Check types
+ run: pnpm run type-check
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8bd551c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/sdks
+!.yarn/versions
+node_modules/
+tsconfig.tsbuildinfo
+dist/
+.yalc
+yalc.lock
+*.tgz
+.DS_Store
+.vscode/
+.vercel
+.next/
+test-results/
+temp/
+*storybook.log
+
+# output of storybook-build
+storybook-static
\ No newline at end of file
diff --git a/.storybook/main.ts b/.storybook/main.ts
new file mode 100644
index 0000000..d7bed82
--- /dev/null
+++ b/.storybook/main.ts
@@ -0,0 +1,25 @@
+import type { StorybookConfig } from "@storybook/react-vite";
+import relay from "vite-plugin-relay";
+import graphqlLoader from "vite-plugin-graphql-loader";
+
+const config: StorybookConfig = {
+ stories: ["./**/*.mdx", "./**/*.stories.@(js|jsx|mjs|ts|tsx)"],
+ addons: [
+ "@storybook/addon-links",
+ "@storybook/addon-essentials",
+ "@storybook/addon-interactions",
+ "@storybook/addon-styling-webpack",
+ "@storybook/addon-docs",
+ ],
+ framework: {
+ name: "@storybook/react-vite",
+ options: {},
+ },
+ async viteFinal(config, options) {
+ // Add your configuration here
+ config.plugins?.push(relay, graphqlLoader());
+ return config;
+ },
+};
+
+export default config;
diff --git a/.storybook/preview.ts b/.storybook/preview.ts
new file mode 100644
index 0000000..65ed8d5
--- /dev/null
+++ b/.storybook/preview.ts
@@ -0,0 +1,23 @@
+import type { Preview } from "@storybook/react";
+import { initialize, mswLoader, getWorker } from "msw-storybook-addon";
+import "./stories/input.css";
+
+// Initialize MSW
+initialize();
+
+const preview: Preview = {
+ // calling getWorker().start() is a workaround for an issue
+ // where Storybook doesn't wait for MSW before running:
+ // https://github.com/mswjs/msw-storybook-addon/issues/89
+ loaders: [mswLoader, () => getWorker().start()],
+ parameters: {
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/i,
+ },
+ },
+ },
+};
+
+export default preview;
diff --git a/.storybook/stories/Apollo.mdx b/.storybook/stories/Apollo.mdx
new file mode 100644
index 0000000..ad42de6
--- /dev/null
+++ b/.storybook/stories/Apollo.mdx
@@ -0,0 +1,46 @@
+import { Canvas, Meta, Source } from '@storybook/blocks';
+import * as ApolloStories from './Apollo.stories.ts';
+
+
+
+# Apollo Demo
+
+The `Apollo/App` and `Apollo/AppWithDefer` stories provide two examples of a MSW handler generated by this library resolving a request originating from a Relay app.
+
+## `App` query
+In `App`, a single JSON response is generated using the mock resolver found in [`src/__tests__/mocks/handlers.ts`](https://github.com/apollographql/graphql-testing-library/blob/main/src/__tests__/mocks/handlers.ts).
+
+
+
+## `AppWithDefer` query
+In `AppWithDefer`, the same mock resolver is used to generate the response, but the presence of `@defer` prompts the generated MSW handler to reply with a multipart response using the proposed [incremental delivery over HTTP](https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md) specification. While the inline fragment is pending, the `Reviews` component displays a `-` in place of the missing data.
+
+
+
+{/* */}
\ No newline at end of file
diff --git a/.storybook/stories/Apollo.stories.ts b/.storybook/stories/Apollo.stories.ts
new file mode 100644
index 0000000..ea27592
--- /dev/null
+++ b/.storybook/stories/Apollo.stories.ts
@@ -0,0 +1,50 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { within, expect, waitFor } from "@storybook/test";
+import {
+ ApolloApp,
+ ApolloAppWithDefer as AppWithDefer,
+} from "./components/apollo-client/ApolloComponent.js";
+import { createHandler } from "../../src/handlers.js";
+import { schemaWithMocks } from "../../src/__tests__/mocks/handlers.js";
+
+const { handler } = createHandler(schemaWithMocks);
+
+const meta = {
+ title: "Example/Apollo",
+ component: ApolloApp,
+ // tags: ["autodocs"],
+ parameters: {
+ layout: "centered",
+ msw: {
+ handlers: {
+ graphql: handler,
+ },
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+
+export { AppWithDefer };
+
+type Story = StoryObj;
+
+export const App: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await expect(
+ canvas.getByRole("heading", { name: /loading/i })
+ ).toHaveTextContent("Loading...");
+ await waitFor(
+ () =>
+ expect(
+ canvas.getByRole("heading", { name: /customers/i })
+ ).toHaveTextContent("Customers also purchased"),
+ { timeout: 2000 }
+ );
+ await waitFor(
+ () => expect(canvas.getByText(/beanie/i)).toBeInTheDocument(),
+ { timeout: 2000 }
+ );
+ },
+};
diff --git a/.storybook/stories/Relay.mdx b/.storybook/stories/Relay.mdx
new file mode 100644
index 0000000..bac46d2
--- /dev/null
+++ b/.storybook/stories/Relay.mdx
@@ -0,0 +1,40 @@
+import { Canvas, Meta, Source } from '@storybook/blocks';
+import * as RelayStories from './Relay.stories.ts';
+
+
+
+# Relay Demo
+
+The `Relay/App` and `Relay/AppWithDefer` stories provide two examples of a MSW handler generated by this library resolving a request originating from a Relay app.
+
+## `App` query
+In `App`, a single JSON response is generated using the mock resolver found in [`src/__tests__/mocks/handlers.ts`](https://github.com/apollographql/graphql-testing-library/blob/main/src/__tests__/mocks/handlers.ts).
+
+
+
+## `AppWithDefer` query
+In `AppWithDefer`, the same mock resolver is used to generate the response, but the presence of `@defer` prompts the generated MSW handler to reply with a multipart response using the proposed [incremental delivery over HTTP](https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md) specification. While the `RelayComponentReviewsFragment_product` fragment is suspending, the `Reviews` component displays a `-` fallback.
+
+
+
+{/* */}
\ No newline at end of file
diff --git a/.storybook/stories/Relay.stories.ts b/.storybook/stories/Relay.stories.ts
new file mode 100644
index 0000000..9cb991b
--- /dev/null
+++ b/.storybook/stories/Relay.stories.ts
@@ -0,0 +1,50 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { within, expect, waitFor } from "@storybook/test";
+import {
+ RelayApp,
+ RelayAppWithDefer as AppWithDefer,
+} from "./components/relay/RelayComponent.js";
+import { createHandler } from "../../src/handlers.js";
+import { schemaWithMocks } from "../../src/__tests__/mocks/handlers.js";
+
+const { handler } = createHandler(schemaWithMocks);
+
+const meta = {
+ title: "Example/Relay",
+ component: RelayApp,
+ // tags: ["autodocs"],
+ parameters: {
+ layout: "centered",
+ msw: {
+ handlers: {
+ graphql: handler,
+ },
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+
+export { AppWithDefer };
+
+type Story = StoryObj;
+
+export const App: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await expect(
+ canvas.getByRole("heading", { name: /loading/i })
+ ).toHaveTextContent("Loading...");
+ await waitFor(
+ () =>
+ expect(
+ canvas.getByRole("heading", { name: /customers/i })
+ ).toHaveTextContent("Customers also purchased"),
+ { timeout: 2000 }
+ );
+ await waitFor(
+ () => expect(canvas.getByText(/beanie/i)).toBeInTheDocument(),
+ { timeout: 2000 }
+ );
+ },
+};
diff --git a/.storybook/stories/Welcome.mdx b/.storybook/stories/Welcome.mdx
new file mode 100644
index 0000000..3977568
--- /dev/null
+++ b/.storybook/stories/Welcome.mdx
@@ -0,0 +1,76 @@
+import { Meta } from "@storybook/blocks";
+
+
+
+
+
GraphQL Testing Library
+
+ {/* */}
+
+
Testing utilities that encourage good practices for apps built with GraphQL.
+
+
+
+
+**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 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 :)
+
+
+## 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";
+
+// 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";
+
+// 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/Container.tsx b/.storybook/stories/components/Container.tsx
new file mode 100644
index 0000000..0fe6270
--- /dev/null
+++ b/.storybook/stories/components/Container.tsx
@@ -0,0 +1,19 @@
+import type { ReactNode } from "react";
+
+function Container({ children }: { children: ReactNode }) {
+ return (
+