diff --git a/.changeset/chatty-llamas-switch.md b/.changeset/chatty-llamas-switch.md deleted file mode 100644 index 334cb9a020c..00000000000 --- a/.changeset/chatty-llamas-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -Adds `createMockFetch` utility for integration testing that includes the link chain diff --git a/.changeset/cold-dancers-call.md b/.changeset/cold-dancers-call.md deleted file mode 100644 index 08885f636e3..00000000000 --- a/.changeset/cold-dancers-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -`createTestSchema` now uses graphql-tools `mergeResolvers` to merge resolvers instead of a shallow merge. diff --git a/.changeset/green-garlics-protect.md b/.changeset/green-garlics-protect.md deleted file mode 100644 index 53dbebb4f55..00000000000 --- a/.changeset/green-garlics-protect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -Rename `createProxiedSchema` to `createTestSchema` and `createMockFetch` to `createSchemaFetch`. diff --git a/.changeset/healthy-chairs-sleep.md b/.changeset/healthy-chairs-sleep.md new file mode 100644 index 00000000000..cfaf23a0a96 --- /dev/null +++ b/.changeset/healthy-chairs-sleep.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Update the `rehackt` dependency to `^0.1.0` diff --git a/.changeset/hungry-bobcats-battle.md b/.changeset/hungry-bobcats-battle.md deleted file mode 100644 index c01bffc7356..00000000000 --- a/.changeset/hungry-bobcats-battle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Adjust `useReadQuery` wrapper logic to work with transported objects. diff --git a/.changeset/kind-foxes-float.md b/.changeset/kind-foxes-float.md deleted file mode 100644 index 0ecc3a14155..00000000000 --- a/.changeset/kind-foxes-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Wrap `useQueryRefHandlers` in `wrapHook`. diff --git a/.changeset/old-onions-sleep.md b/.changeset/old-onions-sleep.md deleted file mode 100644 index e7961c33261..00000000000 --- a/.changeset/old-onions-sleep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Export `WatchFragmentOptions` and `WatchFragmentResult` from main entrypoint and fix bug where `this` wasn't bound to the `watchFragment` method on `ApolloClient`. diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index 9475cefabee..00000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "mode": "pre", - "tag": "rc", - "initialVersions": { - "@apollo/client": "3.9.7" - }, - "changesets": [ - "chatty-llamas-switch", - "cold-dancers-call", - "green-garlics-protect", - "hungry-bobcats-battle", - "kind-foxes-float", - "old-onions-sleep", - "spotty-garlics-knock", - "strong-paws-kneel", - "stupid-bears-cheat", - "tasty-pillows-ring", - "tiny-bugs-tap", - "twelve-apples-vanish", - "wet-plants-admire" - ] -} diff --git a/.changeset/spotty-garlics-knock.md b/.changeset/spotty-garlics-knock.md deleted file mode 100644 index fa58c519668..00000000000 --- a/.changeset/spotty-garlics-knock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -Call `createMockSchema` inside `createTestSchema`. diff --git a/.changeset/strong-paws-kneel.md b/.changeset/strong-paws-kneel.md deleted file mode 100644 index 85262ce36f4..00000000000 --- a/.changeset/strong-paws-kneel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -Add ability to set min and max delay in `createSchemaFetch` diff --git a/.changeset/stupid-bears-cheat.md b/.changeset/stupid-bears-cheat.md deleted file mode 100644 index 636298cae65..00000000000 --- a/.changeset/stupid-bears-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -Adds proxiedSchema and createMockSchema testing utilities diff --git a/.changeset/tasty-pillows-ring.md b/.changeset/tasty-pillows-ring.md deleted file mode 100644 index 5c0f9643bb5..00000000000 --- a/.changeset/tasty-pillows-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -Add `watchFragment` method to the cache and expose it on ApolloClient, refactor `useFragment` using `watchFragment`. diff --git a/.changeset/tiny-bugs-tap.md b/.changeset/tiny-bugs-tap.md deleted file mode 100644 index e45b27b7f56..00000000000 --- a/.changeset/tiny-bugs-tap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -Remove alpha designation for `queryRef.toPromise()` to stabilize the API. diff --git a/.changeset/twelve-apples-vanish.md b/.changeset/twelve-apples-vanish.md deleted file mode 100644 index 99dc9cb6c5f..00000000000 --- a/.changeset/twelve-apples-vanish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -Remove alpha designation for `createQueryPreloader` to stabilize the API. diff --git a/.changeset/wet-plants-admire.md b/.changeset/wet-plants-admire.md deleted file mode 100644 index 4cfd15198e3..00000000000 --- a/.changeset/wet-plants-admire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -Moves new testing utilities to their own entrypoint, `testing/experimental` diff --git a/.github/workflows/snapshot-release.yml b/.github/workflows/snapshot-release.yml index 0db436315f3..6020dddee7d 100644 --- a/.github/workflows/snapshot-release.yml +++ b/.github/workflows/snapshot-release.yml @@ -30,16 +30,47 @@ jobs: startsWith(github.event.comment.body, '/release:pr') steps: - - uses: alessbell/pull-request-comment-branch@v1.1 + - uses: alessbell/pull-request-comment-branch@v2.1.0 id: comment-branch - - name: Checkout head ref + - name: Get sha + id: parse-sha + continue-on-error: true + run: | + if [ "${{ steps.comment-branch.outputs.head_owner }}" == "apollographql" ]; then + echo "sha=${{ steps.comment-branch.outputs.head_sha }}" >> "${GITHUB_OUTPUT}" + else + sha_from_comment="$(echo $COMMENT_BODY | tr -s ' ' | cut -d ' ' -f2)" + + if [ $sha_from_comment == "/release:pr" ]; then + exit 1 + else + echo "sha=$sha_from_comment" >> "${GITHUB_OUTPUT}" + fi + fi + env: + COMMENT_BODY: ${{ github.event.comment.body }} + + - name: Comment sha reminder + if: steps.parse-sha.outcome == 'failure' + uses: peter-evans/create-or-update-comment@v2.1.0 + with: + issue-number: ${{ github.event.issue.number }} + body: | + Did you forget to add the sha? Please use `/release:pr ` + + - name: Fail job + if: steps.parse-sha.outcome == 'failure' + run: | + exit 1 + + - name: Checkout ref uses: actions/checkout@v4 with: ## specify the owner + repository in order to checkout the fork ## for community PRs repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }} - ref: ${{ steps.comment-branch.outputs.head_ref }} + ref: ${{ steps.parse-sha.outputs.sha }} fetch-depth: 0 - name: Detect new changesets @@ -47,7 +78,7 @@ jobs: run: | delimiter="$(openssl rand -hex 8)" echo "changesets<<${delimiter}" >> "${GITHUB_OUTPUT}" - echo "$(git diff --name-only --diff-filter=A ${{ steps.comment-branch.outputs.base_sha }} ${{ steps.comment-branch.outputs.head_sha }} .changeset/*.md)" >> "${GITHUB_OUTPUT}" + echo "$(git diff --name-only --diff-filter=A ${{ steps.comment-branch.outputs.base_sha }} ${{ steps.parse-sha.outputs.sha }} .changeset/*.md)" >> "${GITHUB_OUTPUT}" echo "${delimiter}" >> "${GITHUB_OUTPUT}" - name: Append NPM token to .npmrc diff --git a/.prettierignore b/.prettierignore index fe391b018fc..4af59c9d031 100644 --- a/.prettierignore +++ b/.prettierignore @@ -24,6 +24,7 @@ !/docs/source/development-testing /docs/source/development-testing/** !/docs/source/development-testing/reducing-bundle-size.mdx +!/docs/source/development-testing/schema-driven-testing.mdx !docs/shared /docs/shared/** diff --git a/.size-limits.json b/.size-limits.json index c73d36a02ce..86e387b30fb 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39555, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32831 + "dist/apollo-client.min.cjs": 39534, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32804 } diff --git a/CHANGELOG.md b/CHANGELOG.md index c7347cf2386..29faf90bc17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # @apollo/client +## 3.10.0 + +### Minor Changes + +- [#11605](https://github.com/apollographql/apollo-client/pull/11605) [`e2dd4c9`](https://github.com/apollographql/apollo-client/commit/e2dd4c95290cea604b548cc446826d89aafe8e11) Thanks [@alessbell](https://github.com/alessbell)! - Adds `createMockFetch` utility for integration testing that includes the link chain + +- [#11760](https://github.com/apollographql/apollo-client/pull/11760) [`acd1982`](https://github.com/apollographql/apollo-client/commit/acd1982a59ed66fc44fa9e70b08a31c69dac35a6) Thanks [@alessbell](https://github.com/alessbell)! - `createTestSchema` now uses graphql-tools `mergeResolvers` to merge resolvers instead of a shallow merge. + +- [#11764](https://github.com/apollographql/apollo-client/pull/11764) [`f046aa9`](https://github.com/apollographql/apollo-client/commit/f046aa9fc24ac197a797045d280811a3bbe05806) Thanks [@alessbell](https://github.com/alessbell)! - Rename `createProxiedSchema` to `createTestSchema` and `createMockFetch` to `createSchemaFetch`. + +- [#11777](https://github.com/apollographql/apollo-client/pull/11777) [`5dfc79f`](https://github.com/apollographql/apollo-client/commit/5dfc79fa6d974362f38361f7dffbe984a9546377) Thanks [@alessbell](https://github.com/alessbell)! - Call `createMockSchema` inside `createTestSchema`. + +- [#11774](https://github.com/apollographql/apollo-client/pull/11774) [`2583488`](https://github.com/apollographql/apollo-client/commit/2583488677912cb4500e5fb9e3f91b5c113c4cdb) Thanks [@alessbell](https://github.com/alessbell)! - Add ability to set min and max delay in `createSchemaFetch` + +- [#11605](https://github.com/apollographql/apollo-client/pull/11605) [`e2dd4c9`](https://github.com/apollographql/apollo-client/commit/e2dd4c95290cea604b548cc446826d89aafe8e11) Thanks [@alessbell](https://github.com/alessbell)! - Adds proxiedSchema and createMockSchema testing utilities + +- [#11465](https://github.com/apollographql/apollo-client/pull/11465) [`7623da7`](https://github.com/apollographql/apollo-client/commit/7623da7720855b0c19e13ff9124679f426a39725) Thanks [@alessbell](https://github.com/alessbell)! - Add `watchFragment` method to the cache and expose it on ApolloClient, refactor `useFragment` using `watchFragment`. + +- [#11743](https://github.com/apollographql/apollo-client/pull/11743) [`78891f9`](https://github.com/apollographql/apollo-client/commit/78891f9ec81c0b7a7e010f5550a91965fa33a958) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Remove alpha designation for `queryRef.toPromise()` to stabilize the API. + +- [#11743](https://github.com/apollographql/apollo-client/pull/11743) [`78891f9`](https://github.com/apollographql/apollo-client/commit/78891f9ec81c0b7a7e010f5550a91965fa33a958) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Remove alpha designation for `createQueryPreloader` to stabilize the API. + +- [#11783](https://github.com/apollographql/apollo-client/pull/11783) [`440563a`](https://github.com/apollographql/apollo-client/commit/440563ab2c47efcb9c7d08f52531ade33d753037) Thanks [@alessbell](https://github.com/alessbell)! - Moves new testing utilities to their own entrypoint, `testing/experimental` + +### Patch Changes + +- [#11757](https://github.com/apollographql/apollo-client/pull/11757) [`9825295`](https://github.com/apollographql/apollo-client/commit/982529530893f66a1d236f0fff53862e513fc9a8) Thanks [@phryneas](https://github.com/phryneas)! - Adjust `useReadQuery` wrapper logic to work with transported objects. + +- [#11771](https://github.com/apollographql/apollo-client/pull/11771) [`e72cbba`](https://github.com/apollographql/apollo-client/commit/e72cbba07e5caa6d75b44ca8c766846e855a6c93) Thanks [@phryneas](https://github.com/phryneas)! - Wrap `useQueryRefHandlers` in `wrapHook`. + +- [#11754](https://github.com/apollographql/apollo-client/pull/11754) [`80d2ba5`](https://github.com/apollographql/apollo-client/commit/80d2ba579fe6d2a2d102d1fe79d7d503f31cd931) Thanks [@alessbell](https://github.com/alessbell)! - Export `WatchFragmentOptions` and `WatchFragmentResult` from main entrypoint and fix bug where `this` wasn't bound to the `watchFragment` method on `ApolloClient`. + ## 3.10.0-rc.1 ### Minor Changes diff --git a/ROADMAP.md b/ROADMAP.md index b9ddddb0442..fd4fe2dfdf8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 🔮 Apollo Client Roadmap -**Last updated: 2024-04-08** +**Last updated: 2024-04-22** For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md). @@ -13,14 +13,13 @@ For up to date release notes, refer to the project's [Changelog](https://github. --- -## [3.10.0](https://github.com/apollographql/apollo-client/milestone/33) - April 15th, 2024 +## [3.10.0](https://github.com/apollographql/apollo-client/milestone/33) - April 24th, 2024 -- RC target: April 2nd, 2024 +- Core `watchFragment` API to provide `useFragment`-like functionality for non-React envs +- schema-driven testing utilities ## Upcoming features -- Core `watchFragment` API to provide `useFragment`-like functionality for non-React envs -- schema-driven testing utilities - Data masking - Introduce a suspenseful `useFragment` that will suspend when the data is not yet loaded - leaner client (under alternate entry point) diff --git a/docs/source/api/react/hooks-experimental.mdx b/docs/source/api/react/hooks-experimental.mdx index 99212fa7c30..2557f4e89a9 100644 --- a/docs/source/api/react/hooks-experimental.mdx +++ b/docs/source/api/react/hooks-experimental.mdx @@ -3,4 +3,4 @@ title: Hooks (experimental) description: Apollo Client experimental react hooks API reference --- -The latest minor version of Apollo Client (`3.8`) has no experimental hooks. Please see the [Hooks page](./hooks) for a list of available stable React hooks. +The latest minor version of Apollo Client has no experimental hooks. Please see the [Hooks page](./hooks) for a list of available stable React hooks. diff --git a/docs/source/config.json b/docs/source/config.json index 98c46b99f90..5e85cecba5f 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -49,6 +49,7 @@ "Developer tools": "/development-testing/developer-tooling", "Using TypeScript": "/development-testing/static-typing", "Testing React components": "/development-testing/testing", + "Schema-driven testing": "/development-testing/schema-driven-testing", "Mocking schema capabilities": "/development-testing/client-schema-mocking", "Reducing bundle size": "/development-testing/reducing-bundle-size" }, diff --git a/docs/source/data/queries.mdx b/docs/source/data/queries.mdx index 6eb6326b81b..bd15ebf1a84 100644 --- a/docs/source/data/queries.mdx +++ b/docs/source/data/queries.mdx @@ -370,17 +370,17 @@ new ApolloClient({ options, // The original value of options.fetchPolicy, before nextFetchPolicy was // applied for the first time. - initialPolicy, + initialFetchPolicy, // The ObservableQuery associated with this client.watchQuery call. observable, } ) { // When variables change, the default behavior is to reset - // options.fetchPolicy to context.initialPolicy. If you omit this logic, + // options.fetchPolicy to context.initialFetchPolicy. If you omit this logic, // your nextFetchPolicy function can override this default behavior to // prevent options.fetchPolicy from changing in this case. if (reason === 'variables-changed') { - return initialPolicy; + return initialFetchPolicy; } if ( diff --git a/docs/source/development-testing/schema-driven-testing.mdx b/docs/source/development-testing/schema-driven-testing.mdx new file mode 100644 index 00000000000..3b477829b17 --- /dev/null +++ b/docs/source/development-testing/schema-driven-testing.mdx @@ -0,0 +1,560 @@ +--- +title: Schema-driven testing +description: Using createTestSchema and associated APIs +minVersion: 3.10.0 +--- + +This article describes best practices for writing integration tests using testing utilities released as experimental in v3.10. These testing tools allow developers to execute queries against a schema configured with mock resolvers and default scalar values in order to test an entire Apollo Client application, including the [link chain](/react/api/link/introduction). + +## Guiding principles + +Kent C. Dodds [said it best](https://twitter.com/kentcdodds/status/977018512689455106): + +> The more your tests resemble the way your software is used, the more confidence they can give you. + +When it comes to testing applications built with Apollo Client, this means validating the code path your users' requests will travel from the UI to the network layer and back. + +Unit-style testing with [`MockedProvider`](/react/development-testing/testing) can be useful for testing individual components—or even entire pages or React subtrees—in isolation by mocking the expected response data for individual operations. However, it's important to also test the integration of your components with the network layer. That's where schema-driven testing comes in. + +> This page is heavily inspired by the excellent [Redux documentation](https://redux.js.org/usage/writing-tests#guiding-principles); the same principles apply to Apollo Client. + +## `createTestSchema` and `createSchemaFetch` + +### Installation + +First, ensure you have installed Apollo Client v3.10 or greater. Then, install the following peer dependencies: + +```bash +npm i @graphql-tools/merge @graphql-tools/schema @graphql-tools/utils undici --save-dev +``` + +Consider a React application that fetches a list of products from a GraphQL server: + + + +```tsx title="products.tsx" +import { gql, TypedDocumentNode, useSuspenseQuery } from "@apollo/client"; + +type ProductsQuery = { + products: Array<{ + __typename: "Product"; + id: string; + title: string; + mediaUrl: string; + }>; +}; + +const PRODUCTS_QUERY: TypedDocumentNode = gql` + query ProductsQuery { + products { + id + title + mediaUrl + } + } +`; + +export function Products() { + const { data } = useSuspenseQuery(PRODUCTS_QUERY); + + return ( +
+ {data.products.map((product) => ( +

+ + {product.title} - {product.id} + +

+ ))} +
+ ); +} +``` + +
+ +Now let's write some tests using a test schema created with the `createTestSchema` utility that can then be used to create a mock fetch implementation with `createSchemaFetch`. + +### Configuring your test environment + +First, some Node.js globals will need to be polyfilled in order for JSDOM tests to run correctly. Create a file called e.g. `jest.polyfills.js`: + +```js title="jest.polyfills.js" +/** + * @note The block below contains polyfills for Node.js globals + * required for Jest to function when running JSDOM tests. + * These have to be require's and have to be in this exact + * order, since "undici" depends on the "TextEncoder" global API. + */ + +const { TextDecoder, TextEncoder } = require("node:util"); +const { ReadableStream } = require("node:stream/web"); +const { clearImmediate } = require("node:timers"); +const { performance } = require("node:perf_hooks"); + +Object.defineProperties(globalThis, { + TextDecoder: { value: TextDecoder }, + TextEncoder: { value: TextEncoder }, + ReadableStream: { value: ReadableStream }, + performance: { value: performance }, + clearImmediate: { value: clearImmediate }, +}); + +const { Blob, File } = require("node:buffer"); +const { fetch, Headers, FormData, Request, Response } = require("undici"); + +Object.defineProperties(globalThis, { + fetch: { value: fetch, writable: true }, + Response: { value: Response }, + Blob: { value: Blob }, + File: { value: File }, + Headers: { value: Headers }, + FormData: { value: FormData }, + Request: { value: Request }, +}); + +// Note: if your environment supports it, you can use the `using` keyword +// but must polyfill Symbol.dispose here with Jest versions <= 29 +// where Symbol.dispose is not defined +// +// Jest bug: https://github.com/jestjs/jest/issues/14874 +// Fix is available in https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.3 +if (!Symbol.dispose) { + Object.defineProperty(Symbol, "dispose", { + value: Symbol("dispose"), + }); +} +if (!Symbol.asyncDispose) { + Object.defineProperty(Symbol, "asyncDispose", { + value: Symbol("asyncDispose"), + }); +} +``` + +Now, in a `jest.config.ts` or `jest.config.js` file, add the following configuration: + +```ts title="jest.config.ts" +import type { Config } from "jest"; + +const config: Config = { + globals: { + "globalThis.__DEV__": JSON.stringify(true), + }, + testEnvironment: "jsdom", + setupFiles: ["./jest.polyfills.js"], + // You may also have an e.g. setupTests.ts file here + setupFilesAfterEnv: ["/setupTests.ts"], + // If you're using MSW, opt out of the browser export condition for MSW tests + // For more information, see: https://github.com/mswjs/msw/issues/1786#issuecomment-1782559851 + testEnvironmentOptions: { + customExportConditions: [""], + }, + // If you plan on importing .gql/.graphql files in your tests, transform them with @graphql-tools/jest-transform + transform: { + "\\.(gql|graphql)$": "@graphql-tools/jest-transform", + }, +}; + +export default config; +``` + +In the example `setupTests.ts` file below, `@testing-library/jest-dom` is imported to allow the use of custom `jest-dom` matchers (see the [`@testing-library/jest-dom` documentation](https://github.com/testing-library/jest-dom?tab=readme-ov-file#usage) for more information) and fragment warnings are disabled which can pollute the test output: + +```ts title="setupTests.ts" +import "@testing-library/jest-dom"; +import { gql } from "@apollo/client"; + +gql.disableFragmentWarnings(); +``` + +### Testing with MSW + +Now, let's write a test for the `Products` component using [MSW](https://mswjs.io/). + +MSW is a powerful tool for intercepting network traffic and mocking responses. Read more about its design and philosophy [here](https://mswjs.io/blog/why-mock-service-worker/). + +MSW has the concept of [handlers](https://mswjs.io/docs/best-practices/structuring-handlers/) that allow network requests to be intercepted. Let's create a handler that will intercept all GraphQL operations: + +```ts title="src/__tests__/handlers.ts" +import { graphql, HttpResponse } from "msw"; +import { execute } from "graphql"; +import type { ExecutionResult } from "graphql"; +import type { ObjMap } from "graphql/jsutils/ObjMap"; +import { gql } from "@apollo/client"; +import { createTestSchema } from "@apollo/client/testing/experimental"; +import { makeExecutableSchema } from "@graphql-tools/schema"; +import graphqlSchema from "../../../schema.graphql"; + +// First, create a static schema... +const staticSchema = makeExecutableSchema({ typeDefs: graphqlSchema }); + +// ...which is then passed as the first argument to `createTestSchema` +// along with mock resolvers and default scalar values. +export let testSchema = createTestSchema(staticSchema, { + resolvers: { + Query: { + products: () => [ + { + id: "1", + title: "Blue Jays Hat", + }, + ], + }, + }, + scalars: { + Int: () => 6, + Float: () => 22.1, + String: () => "string", + }, +}); + +export const handlers = [ + // Intercept all GraphQL operations and return a response generated by the + // test schema. Add additional handlers as needed. + graphql.operation, ObjMap>>( + async ({ query, variables, operationName }) => { + const document = gql(query); + + const result = await execute({ + document, + operationName, + schema: testSchema, + variableValues: variables, + }); + + return HttpResponse.json(result); + } + ), +]; +``` + +MSW can be used in [the browser](https://mswjs.io/docs/integrations/browser), in [Node.js](https://mswjs.io/docs/integrations/node) and in [React Native](https://mswjs.io/docs/integrations/react-native). Since this example is using Jest and JSDOM to run tests in a Node.js environment, let's configure the server per the [Node.js integration guide](https://mswjs.io/docs/integrations/node): + +```ts title="src/__tests__/server.ts" +import { setupServer } from "msw/node"; +import { handlers } from "./handlers"; + +// This configures a request mocking server with the given request handlers. +export const server = setupServer(...handlers); +``` + +Finally, let's do server set up and teardown in the `setupTests.ts` file created in the previous step: + +```ts title="setupTests.ts" {6-8} +import "@testing-library/jest-dom"; +import { gql } from "@apollo/client"; + +gql.disableFragmentWarnings(); + +beforeAll(() => server.listen({ onUnhandledRequest: "error" })); +afterAll(() => server.close()); +afterEach(() => server.resetHandlers()); +``` + +Finally, let's write some tests 🎉 + +```tsx title="src/__tests__/products.test.tsx" +import { Suspense } from "react"; +import { render as rtlRender, screen } from "@testing-library/react"; +import { + ApolloClient, + ApolloProvider, + NormalizedCacheObject, +} from "@apollo/client"; +import { testSchema } from "./handlers"; +import { Products } from "../products"; +// This should be a function that returns a new ApolloClient instance +// configured just like your production Apollo Client instance - see the FAQ. +import { makeClient } from "../client"; + +const render = (renderedClient: ApolloClient) => + rtlRender( + + + + + + ); + +describe("Products", () => { + test("renders", async () => { + render(makeClient()); + + await screen.findByText("Loading..."); + + // This is the data from our initial mock resolver in the test schema + // defined in the handlers file 🎉 + expect(await screen.findByText(/blue jays hat/i)).toBeInTheDocument(); + }); + + test("allows resolvers to be updated via .add", async () => { + // Calling .add on the test schema will update the resolvers + // with new data + testSchema.add({ + resolvers: { + Query: { + products: () => { + return [ + { + id: "2", + title: "Mets Hat", + }, + ]; + }, + }, + }, + }); + + render(makeClient()); + + await screen.findByText("Loading..."); + + // Our component now renders the new data from the updated resolver + await screen.findByText(/mets hat/i); + }); + + test("handles test schema resetting via .reset", async () => { + // Calling .reset on the test schema will reset the resolvers + testSchema.reset(); + + render(makeClient()); + + await screen.findByText("Loading..."); + + // The component now renders the initial data configured on the test schema + await screen.findByText(/blue jays hat/i); + }); +}); +``` + +### Testing by mocking fetch with `createSchemaFetch` + +First, import `createSchemaFetch` and `createTestSchema` from the new `@apollo/client/testing` entrypoint. Next, import a local copy of your graph's schema: jest should be configured to transform `.gql` or `.graphql` files using `@graphql-tools/jest-transform` (see the `jest.config.ts` example configuration above.) + +Here's how an initial set up of the test file might look: + +```tsx title="products.test.tsx" +import { + createSchemaFetch, + createTestSchema, +} from "@apollo/client/testing/experimental"; +import { makeExecutableSchema } from "@graphql-tools/schema"; +import { render as rtlRender, screen } from "@testing-library/react"; +import graphqlSchema from "../../../schema.graphql"; +// This should be a function that returns a new ApolloClient instance +// configured just like your production Apollo Client instance - see the FAQ. +import { makeClient } from "../../client"; +import { ApolloProvider, NormalizedCacheObject } from "@apollo/client"; +import { Products } from "../../products"; +import { Suspense } from "react"; + +// First, let's create an executable schema... +const staticSchema = makeExecutableSchema({ typeDefs: graphqlSchema }); + +// which is then passed as the first argument to `createTestSchema`. +const schema = createTestSchema(staticSchema, { + // Next, let's define mock resolvers + resolvers: { + Query: { + products: () => + Array.from({ length: 5 }, (_element, id) => ({ + id, + mediaUrl: `https://example.com/image${id}.jpg`, + })), + }, + }, + // ...and default scalar values + scalars: { + Int: () => 6, + Float: () => 22.1, + String: () => "default string", + }, +}); + +// This `render` helper function would typically be extracted and shared between +// test files. +const render = (renderedClient: ApolloClient) => + rtlRender( + + + + + + ); +``` + +Now let's write some tests 🎉 + +First, `createSchemaFetch` can be used to mock the global `fetch` implementation with one that resolves network requests with payloads generated from the test schema. + +```tsx title="products.test.tsx" +describe("Products", () => { + it("renders", async () => { + using _fetch = createSchemaFetch(schema).mockGlobal(); + + render(makeClient()); + + await screen.findByText("Loading..."); + + // title is rendering the default string scalar + const findAllByText = await screen.findAllByText(/default string/); + expect(findAllByText).toHaveLength(5); + + // the products resolver is returning 5 products + await screen.findByText(/0/); + await screen.findByText(/1/); + await screen.findByText(/2/); + await screen.findByText(/3/); + await screen.findByText(/4/); + }); +}); +``` + +#### A note on `using` and explicit resource management + +You may have noticed a new keyword in the first line of the test above: `using`. + +`using` is part of a [proposed new language feature](https://github.com/tc39/proposal-explicit-resource-management) which is currently at Stage 3 of the TC39 proposal process. + +If you are using TypeScript 5.2 or greater, or using Babel's [`@babel/plugin-proposal-explicit-resource-management` plugin](https://babeljs.io/docs/babel-plugin-proposal-explicit-resource-management), you can use the `using` keyword to automatically perform some cleanup when `_fetch` goes out of scope. In our case, this is when the test is complete; this means restoring the global fetch function to its original state automatically after each test. + +If your environment does not support explicit resource management, you'll find that calling `mockGlobal()` returns a restore function that you can manually call at the end of each test: + +```tsx title="products.test.tsx" +describe("Products", () => { + it("renders", async () => { + const { restore } = createSchemaFetch(schema).mockGlobal(); + + render(makeClient()); + + // make assertions against the rendered DOM output + + restore(); + }); +}); +``` + +## Modifying a test schema using `testSchema.add` and `testSchema.fork` + +If you need to make changes to the behavior of a schema after it has been created, you can use the `testSchema.add` method to add new resolvers to the schema or overwrite existing ones. +This can be useful for testing scenarios where the behavior of the schema needs to change inside a test. + +````tsx title="products.test.tsx" +describe("Products", () => { + it("renders", async () => { + const { restore } = createSchemaFetch(schema).mockGlobal(); + + render(makeClient()); + + // make assertions against the rendered DOM output + + // Here we want to change the return value of the `products` resolver + // for the next outgoing query. + testSchema.add({ + resolvers: { + Query: { + products: () => + Array.from({ length: 5 }, (_element, id) => ({ + // we want to return ids starting from 5 for the second request + id: id + 5, + mediaUrl: `https://example.com/image${id + 5}.jpg`, + })), + }, + }, + }); + + // trigger a new query with a user interaction + userEvent.click(screen.getByText("Fetch more")); + + // make assertions against the rendered DOM output + + restore(); + testSchema.reset(); + }); +}); +``` + +Alternatively, you can use `testSchema.fork` to create a new schema with the same configuration as the original schema, +but with the ability to make changes to the new isolated schema without affecting the original schema. +This can be useful if you just want to mock the global fetch function with a different schema for each test without +having to care about resetting your original testSchema. +You could also write incremental tests where each test builds on the previous one. + +If you use MSW, you will probably end up using `testSchema.add`, as MSW needs to be set up with a single schema for all tests. +If you are going the `createSchemaFetch` route, you can use `testSchema.fork` to create a new schema for each test, +and then use `forkedSchema.add` to make changes to the schema for that test. + +```tsx +const baseSchema = createTestSchema(staticSchema, { + resolvers: { + // ... + }, + scalars: { + // ... + }, +}); + +test("a test", () => { + const forkedSchema = baseSchema.fork(); + + const { restore } = createSchemaFetch(forkedSchema).mockGlobal(); + + // make assertions against the rendered DOM output + + forkedSchema.add({ + // ... + }); + + restore(); + // forkedSchema will just be discarded, and there is no need to reset it +}); +```` + +### FAQ + +#### When should I use `createSchemaFetch` vs [MSW](https://mswjs.io/)? + +There are many benefits to using [MSW](https://mswjs.io/): it's a powerful tool with a great set of APIs. Read more about its philosophy and benefits [here](https://mswjs.io/blog/why-mock-service-worker/). + +Wherever possible, use MSW: it enables more realistic tests that can catch more bugs by intercepting requests _after_ they've been dispatched by an application. MSW also supports both REST and GraphQL handlers, so if your application uses a combination (e.g. to fetch data from a third party endpoint), MSW will provide more flexibility than `createSchemaFetch`, which is a more lightweight solution. + +#### Should I share a single `ApolloClient` instance between tests? + +No; please create a new instance of `ApolloClient` for each test. Even if the cache is reset in between tests, the client maintains some internal state that is not reset. This could have some unintended consequences. For example, the `ApolloClient` instance could have pending queries that could cause the following test's queries to be deduplicated by default. + +Instead, create a `makeClient` function or equivalent so that every test uses the same client configuration as your production client, but no two tests share the same client instance. Here's an example: + + + +```ts title="src/client.ts" +import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; + +const httpLink = new HttpLink({ + uri: "https://example.com/graphql", +}); + +export const makeClient = () => { + return new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + }); +}; + +export const client = makeClient(); +``` + + + +This way, every test can use `makeClient` to create a new client instance, and you can still use `client` in your production code. + +#### Can I use these testing tools with Vitest? + +Unfortunately not at the moment. This is caused by a known limitation with the `graphql` package and tools that bundle ESM by default known as the [dual package hazard](https://nodejs.org/api/packages.html#dual-package-hazard). + +Please see [this issue](https://github.com/graphql/graphql-js/issues/4062) to track the related discussion on the `graphql/graphql-js` repository. + +## Sandbox example + +For a working example that demonstrates how to use both Testing Library and Mock Service Worker to write integration tests with `createTestSchema`, check out this project on CodeSandbox: + +[![Edit Testing React Components](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/apollographql/docs-examples/tree/main/apollo-client/v3/testing-react-components?file=/src/dog.test.js) diff --git a/docs/source/development-testing/static-typing.md b/docs/source/development-testing/static-typing.md index 3d11003c0c3..96a5148550c 100644 --- a/docs/source/development-testing/static-typing.md +++ b/docs/source/development-testing/static-typing.md @@ -16,7 +16,7 @@ Below, we'll guide you through installing and configuring GraphQL Code Generator To get started using GraphQL Code Generator, begin by installing the following packages (using Yarn or NPM): ```bash -yarn add -D typescript @graphql-codegen/cli @graphql-codegen/client-preset @graphql-typed-document-node/core +yarn add -D typescript graphql @graphql-codegen/cli @graphql-codegen/client-preset @graphql-typed-document-node/core ``` Next, we'll create a configuration file for GraphQL Code Generator, named [`codegen.ts`](https://www.the-guild.dev/graphql/codegen/docs/config-reference/codegen-config), at the root of our project: diff --git a/docs/source/index.mdx b/docs/source/index.mdx index 4d4d59f2e68..6c6565888e1 100644 --- a/docs/source/index.mdx +++ b/docs/source/index.mdx @@ -4,29 +4,46 @@ title: Introduction to Apollo Client import { Link } from 'gatsby'; -**Apollo Client** is a comprehensive state management library for JavaScript that enables you to manage both local and remote data with GraphQL. Use it to fetch, cache, and modify application data, all while automatically updating your UI. +**Apollo Client** is a comprehensive state management library for JavaScript. It enables you to manage both local and remote data with GraphQL. Use it to fetch, cache, and modify application data, all while automatically updating your UI. Apollo Client helps you structure code in an economical, predictable, and declarative way that's consistent with modern development practices. The core `@apollo/client` library provides built-in integration with React, and the larger Apollo community maintains [integrations for other popular view layers](#community-integrations).

Get started!

-## Features +## Core features + +Some of Apollo Client's core capabilities include: - **Declarative data fetching:** Write a query and receive data without manually tracking loading states. +- **Normalized request and response caching:** Boost performance by responding almost immediately to queries with cached data. - **Excellent developer experience:** Enjoy helpful tooling for TypeScript, Chrome / Firefox devtools, and VS Code. -- **Designed for modern React:** Take advantage of the latest React features, such as hooks. -- **Incrementally adoptable:** Drop Apollo into any JavaScript app and incorporate it feature by feature. +- **Designed for modern React:** Take advantage of the latest React features, such as hooks and Suspense. +- **Incrementally adoptable:** Drop Apollo Client into any JavaScript app and incorporate it feature by feature. - **Universally compatible:** Use any build setup and any GraphQL API. - **Community driven:** Share knowledge with thousands of developers in the GraphQL community. +## GraphOS supported features + +Apollo Client works seamlessly with these GraphOS router supported features: + +- Receiving data for specific fields incrementally with the [`@defer` directive](/graphos/operations/defer) +- Real-time updates via [GraphQL subscriptions](/graphos/operations/subscriptions) +- Safelisting with [persisted queries](/graphos/operations/persisted-queries) + + + +Apollo Client also supports `@defer` and GraphQL subscription implementations outside of GraphOS. + + + ## Recommended docs After you [get started](./get-started/), check out the full Apollo Client documentation in the navigation on the left. diff --git a/package-lock.json b/package-lock.json index a989dc52e69..0d79b181395 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.10.0-rc.1", + "version": "3.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.10.0-rc.1", + "version": "3.10.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -18,7 +18,7 @@ "hoist-non-react-statics": "^3.3.2", "optimism": "^0.18.0", "prop-types": "^15.7.2", - "rehackt": "0.0.6", + "rehackt": "^0.1.0", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", @@ -10691,9 +10691,9 @@ } }, "node_modules/rehackt": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.0.6.tgz", - "integrity": "sha512-l3WEzkt4ntlEc/IB3/mF6SRgNHA6zfQR7BlGOgBTOmx7IJJXojDASav+NsgXHFjHn+6RmwqsGPFgZpabWpeOdw==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz", + "integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==", "peerDependencies": { "@types/react": "*", "react": "*" diff --git a/package.json b/package.json index 8074fc444ff..a4157dcc021 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.10.0-rc.1", + "version": "3.10.0", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ @@ -99,7 +99,7 @@ "hoist-non-react-statics": "^3.3.2", "optimism": "^0.18.0", "prop-types": "^15.7.2", - "rehackt": "0.0.6", + "rehackt": "^0.1.0", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", diff --git a/src/core/__tests__/fetchPolicies.ts b/src/core/__tests__/fetchPolicies.ts index 6f1f1cac5e9..f5ed3743f4b 100644 --- a/src/core/__tests__/fetchPolicies.ts +++ b/src/core/__tests__/fetchPolicies.ts @@ -1274,7 +1274,7 @@ describe("nextFetchPolicy", () => { // The nextFetchPolicy function we provided always returnes cache-first, // even when context.reason is variables-changed (which by default - // resets the fetchPolicy to context.initialPolicy), so cache-first is + // resets the fetchPolicy to context.initialFetchPolicy), so cache-first is // still what we see here. expect(observable.options.fetchPolicy).toBe("cache-first"); } else if (count === 3) { diff --git a/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx b/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx index a8cf0f0a69a..0bf53ed8ab5 100644 --- a/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx +++ b/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx @@ -1,5 +1,5 @@ /* eslint-disable testing-library/render-result-naming-convention */ -import React, { useEffect } from "react"; +import React, { useEffect } from "rehackt"; import { useRenderGuard } from "../useRenderGuard"; import { render, waitFor } from "@testing-library/react"; import { withCleanup } from "../../../../testing/internal"; diff --git a/src/testing/internal/profile/Render.tsx b/src/testing/internal/profile/Render.tsx index 0757392b26d..284594c9f51 100644 --- a/src/testing/internal/profile/Render.tsx +++ b/src/testing/internal/profile/Render.tsx @@ -124,7 +124,7 @@ export class RenderInstance implements Render { return (this._domSnapshot = body); } - get withinDOM() { + get withinDOM(): () => SyncScreen { const snapScreen = Object.assign(within(this.domSnapshot), { debug: ( ...[dom = this.domSnapshot, ...rest]: Parameters