Repository: FormidableLabs/urql
Pull request #322 (2019-06-26 22:34:40)

adjust next.js example to be more aligned  with apollo example from the Readme
Here is the Pull-Request https://github.com/zeit/next.js/pull/7651 to Canary release.


Patch:
diff --git a/examples/3-ssr-with-nextjs/src/init-urql-client.js b/examples/3-ssr-with-nextjs/src/init-urql-client.js
index 5d226126..7e161427 100644
--- a/examples/3-ssr-with-nextjs/src/init-urql-client.js
+++ b/examples/3-ssr-with-nextjs/src/init-urql-client.js
@@ -15,13 +15,14 @@ export default function initUrqlClient(initialState) {
   // Create a new client for every server-side rendered request to reset its state
   // for each rendered page
   // Reuse the client on the client-side however
-  if (!process.browser || !urqlClient) {
+  const isServer = typeof window === 'undefined';
+  if (isServer || !urqlClient) {
     ssrCache = ssrExchange({ initialState });
 
     urqlClient = createClient({
       url: 'https://api.graph.cool/simple/v1/cixmkt2ul01q00122mksg82pn',
       // Active suspense mode on the server-side
-      suspense: !process.browser,
+      suspense: isServer,
       exchanges: [dedupExchange, cacheExchange, ssrCache, fetchExchange],
     });
   }
diff --git a/examples/3-ssr-with-nextjs/src/with-urql-client.js b/examples/3-ssr-with-nextjs/src/with-urql-client.js
index 4eb08d4b..a8af6181 100644
--- a/examples/3-ssr-with-nextjs/src/with-urql-client.js
+++ b/examples/3-ssr-with-nextjs/src/with-urql-client.js
@@ -16,7 +16,8 @@ const withUrqlClient = App => {
 
       // getInitialProps is universal, but we only want
       // to run server-side rendered suspense on the server
-      if (process.browser) {
+      const isBrowser = typeof window !== "undefined";
+      if (isBrowser) {
         return appProps;
       }
 
diff --git a/src/exchanges/cache.ts b/src/exchanges/cache.ts
index 762a3876..8c66e402 100755
--- a/src/exchanges/cache.ts
+++ b/src/exchanges/cache.ts
@@ -3,7 +3,11 @@ import { filter, map, merge, pipe, share, tap } from 'wonka';
 
 import { Client } from '../client';
 import { Exchange, Operation, OperationResult } from '../types';
-import { collectTypesFromResponse, formatDocument } from '../utils/typenames';
+import {
+  addMetadata,
+  collectTypesFromResponse,
+  formatDocument,
+} from '../utils';
 
 type ResultCache = Map<number, OperationResult>;
 
@@ -52,23 +56,25 @@ export const cacheExchange: Exchange = ({ forward, client }) => {
       sharedOps$,
       filter(op => !shouldSkip(op) && isOperationCached(op)),
       map(operation => {
-        const {
-          key,
-          context: { requestPolicy },
-        } = operation;
+        const { key, context } = operation;
         const cachedResult = resultCache.get(key);
-        if (requestPolicy === 'cache-and-network') {
+        if (context.requestPolicy === 'cache-and-network') {
           reexecuteOperation(client, operation);
         }
 
         if (cachedResult !== undefined) {
-          return cachedResult;
+          return {
+            ...cachedResult,
+            operation: addMetadata(cachedResult.operation, {
+              cacheOutcome: 'hit',
+            }),
+          };
         }
 
         return {
-          operation,
           data: undefined,
           error: undefined,
+          operation: addMetadata(operation, { cacheOutcome: 'miss' }),
         };
       })
     );
@@ -85,6 +91,7 @@ export const cacheExchange: Exchange = ({ forward, client }) => {
           filter(op => shouldSkip(op))
         ),
       ]),
+      map(op => addMetadata(op, { cacheOutcome: 'miss' })),
       forward,
       tap(response => {
         if (
@@ -145,19 +152,17 @@ const afterQuery = (
   resultCache: ResultCache,
   operationCache: OperationCache
 ) => (response: OperationResult) => {
-  const {
-    operation: { key },
-    data,
-  } = response;
+  const { operation, data } = response;
+
   if (data === undefined) {
     return;
   }
 
-  resultCache.set(key, response);
+  resultCache.set(operation.key, response);
 
   collectTypesFromResponse(response.data).forEach(typeName => {
     const operations =
       operationCache[typeName] || (operationCache[typeName] = new Set());
-    operations.add(key);
+    operations.add(operation.key);
   });
 };
diff --git a/src/exchanges/fetch.ts b/src/exchanges/fetch.ts
index b54a6ea7..9208e6eb 100755
--- a/src/exchanges/fetch.ts
+++ b/src/exchanges/fetch.ts
@@ -2,7 +2,7 @@
 import { print } from 'graphql';
 import { filter, make, merge, mergeMap, pipe, share, takeUntil } from 'wonka';
 import { Exchange, Operation, OperationResult } from '../types';
-import { CombinedError } from '../utils/error';
+import { addMetadata, CombinedError } from '../utils';
 
 /** A default exchange for fetching GraphQL requests. */
 export const fetchExchange: Exchange = ({ forward }) => {
@@ -75,9 +75,15 @@ const createFetchSource = (operation: Operation) => {
         abortController !== undefined ? abortController.signal : undefined,
     };
 
+    const startTime = Date.now();
     executeFetch(operation, fetchOptions).then(result => {
       if (result !== undefined) {
-        next(result);
+        next({
+          ...result,
+          operation: addMetadata(result.operation, {
+            networkLatency: Date.now() - startTime,
+          }),
+        });
       }
 
       complete();
diff --git a/src/hooks/useDevtoolsContext.ts b/src/hooks/useDevtoolsContext.ts
index c7b57c2c..dc10d668 100644
--- a/src/hooks/useDevtoolsContext.ts
+++ b/src/hooks/useDevtoolsContext.ts
@@ -18,7 +18,7 @@ const getHookParent = () => {
 const useDevtoolsContextHook = () => {
   const source = useRef(getHookParent());
 
-  return useMemo(() => [{ devtools: { source: source.current } }], []);
+  return useMemo(() => [{ meta: { source: source.current } }], []);
 };
 
 /** Creates additional context values for serving metadata to devtools. */
diff --git a/src/types.ts b/src/types.ts
index 464b3c4c..5eb12b8a 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -18,6 +18,9 @@ export type RequestPolicy =
   | 'network-only'
   | 'cache-and-network';
 
+/** How the operation has */
+export type CacheOutcome = 'miss' | 'partial' | 'hit';
+
 /** A Graphql query, mutation, or subscription. */
 export interface GraphQLRequest {
   /** Unique identifier of the request. */
@@ -32,6 +35,11 @@ export interface OperationContext {
   fetchOptions?: RequestInit | (() => RequestInit);
   requestPolicy: RequestPolicy;
   url: string;
+  meta?: {
+    source?: string;
+    cacheOutcome?: CacheOutcome;
+    networkLatency?: number;
+  };
 }
 
 /** A [query]{@link Query} or [mutation]{@link Mutation} with additional metadata for use during transmission. */
diff --git a/src/utils/request.ts b/src/utils/request.ts
index 0f01129c..a81f8763 100644
--- a/src/utils/request.ts
+++ b/src/utils/request.ts
@@ -1,7 +1,7 @@
 import { DocumentNode } from 'graphql';
 import gql from 'graphql-tag';
 import { getKeyForRequest } from './keyForQuery';
-import { GraphQLRequest } from '../types';
+import { GraphQLRequest, Operation, OperationContext } from '../types';
 
 export const createRequest = (
   q: string | DocumentNode,
@@ -15,3 +15,18 @@ export const createRequest = (
     variables: vars || {},
   };
 };
+
+/** Spreads the provided metadata to the source operation's meta property in context.  */
+export const addMetadata = (
+  source: Operation,
+  meta: Exclude<OperationContext['meta'], undefined>
+) => ({
+  ...source,
+  context: {
+    ...source.context,
+    meta: {
+      ...source.context.meta,
+      ...meta,
+    },
+  },
+});
diff --git a/src/utils/typenames.ts b/src/utils/typenames.ts
index 2fa9cc2b..384b594f 100755
--- a/src/utils/typenames.ts
+++ b/src/utils/typenames.ts
@@ -35,25 +35,36 @@ export const collectTypesFromResponse = (response: object) =>
 
 const formatNode = (
   n: FieldNode | InlineFragmentNode | OperationDefinitionNode
-) =>
-  n.selectionSet !== undefined && n.selectionSet.selections !== undefined
-    ? {
-        ...n,
-        selectionSet: {
-          ...n.selectionSet,
-          selections: [
-            ...n.selectionSet.selections,
-            {
-              kind: 'Field',
-              name: {
-                kind: 'Name',
-                value: '__typename',
-              },
-            },
-          ],
+) => {
+  if (n.selectionSet === undefined) {
+    return false;
+  }
+
+  if (
+    n.selectionSet.selections.some(
+      s => s.kind === 'Field' && s.name.value === '__typename'
+    )
+  ) {
+    return n;
+  }
+
+  return {
+    ...n,
+    selectionSet: {
+      ...n.selectionSet,
+      selections: [
+        ...n.selectionSet.selections,
+        {
+          kind: 'Field',
+          name: {
+            kind: 'Name',
+            value: '__typename',
+          },
         },
-      }
-    : false;
+      ],
+    },
+  };
+};
 
 export const formatDocument = (astNode: DocumentNode) =>
   visit(astNode, {


Test patch:
diff --git a/src/exchanges/__snapshots__/fetch.test.ts.snap b/src/exchanges/__snapshots__/fetch.test.ts.snap
index ad804f92..13a0174b 100644
--- a/src/exchanges/__snapshots__/fetch.test.ts.snap
+++ b/src/exchanges/__snapshots__/fetch.test.ts.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`returns error data from fetch 1`] = `
+exports[`on error returns error data 1`] = `
 Object {
   "data": undefined,
   "error": [CombinedError: [Network] ],
@@ -9,6 +9,9 @@ Object {
       "fetchOptions": Object {
         "method": "POST",
       },
+      "meta": Object {
+        "networkLatency": 100,
+      },
       "requestPolicy": "cache-first",
       "url": "http://localhost:3000/graphql",
     },
@@ -128,7 +131,7 @@ Object {
 }
 `;
 
-exports[`returns response data from fetch 1`] = `
+exports[`on success returns response data 1`] = `
 Object {
   "data": Object {
     "data": Object {
@@ -149,6 +152,9 @@ Object {
           },
         ],
       },
+      "meta": Object {
+        "networkLatency": 100,
+      },
       "requestPolicy": "cache-first",
       "url": "http://localhost:3000/graphql",
     },
diff --git a/src/exchanges/cache.test.ts b/src/exchanges/cache.test.ts
index c3fd5cb3..09a44b07 100755
--- a/src/exchanges/cache.test.ts
+++ b/src/exchanges/cache.test.ts
@@ -1,4 +1,13 @@
-import { makeSubject, map, pipe, publish, Source, Subject } from 'wonka';
+import {
+  makeSubject,
+  map,
+  pipe,
+  publish,
+  Source,
+  Subject,
+  scan,
+  toPromise,
+} from 'wonka';
 import { Client } from '../client';
 import {
   mutationOperation,
@@ -9,7 +18,7 @@ import {
   subscriptionResult,
   undefinedQueryResponse,
 } from '../test-utils';
-import { Operation } from '../types';
+import { Operation, OperationResult } from '../types';
 import { afterMutation, cacheExchange } from './cache';
 
 let response;
@@ -42,76 +51,121 @@ beforeEach(() => {
   exchangeArgs = { forward, client };
 });
 
-it('forwards to next exchange when no cache is found', () => {
-  const [ops$, next, complete] = input;
-  const exchange = cacheExchange(exchangeArgs)(ops$);
+describe('on query', () => {
+  it('forwards to next exchange when no cache hit', () => {
+    const [ops$, next, complete] = input;
+    const exchange = cacheExchange(exchangeArgs)(ops$);
 
-  publish(exchange);
-  next(queryOperation);
-  complete();
-  expect(forwardedOperations.length).toBe(1);
-  expect(reexecuteOperation).not.toBeCalled();
-});
+    publish(exchange);
+    next(queryOperation);
+    complete();
+    expect(forwardedOperations.length).toBe(1);
+    expect(reexecuteOperation).not.toBeCalled();
+  });
+
+  it('caches results', () => {
+    const [ops$, next, complete] = input;
+    const exchange = cacheExchange(exchangeArgs)(ops$);
 
-it('caches queries', () => {
-  const [ops$, next, complete] = input;
-  const exchange = cacheExchange(exchangeArgs)(ops$);
+    publish(exchange);
+    next(queryOperation);
+    next(queryOperation);
+    complete();
+    expect(forwardedOperations.length).toBe(1);
+    expect(reexecuteOperation).not.toBeCalled();
+  });
 
-  publish(exchange);
-  next(queryOperation);
-  next(queryOperation);
-  complete();
-  expect(forwardedOperations.length).toBe(1);
-  expect(reexecuteOperation).not.toBeCalled();
-});
+  describe('cache hit', () => {
+    it('is miss when operation is forwarded', () => {
+      const [ops$, next, complete] = input;
+      const exchange = cacheExchange(exchangeArgs)(ops$);
+
+      publish(exchange);
+      next(queryOperation);
+      complete();
+
+      expect(forwardedOperations[0].context).toHaveProperty(
+        'meta.cacheOutcome',
+        'miss'
+      );
+    });
 
-it("doesn't cache mutations", () => {
-  response = mutationResponse;
-  const [ops$, next, complete] = input;
-  const exchange = cacheExchange(exchangeArgs)(ops$);
-
-  publish(exchange);
-  next(mutationOperation);
-  next(mutationOperation);
-  complete();
-  expect(forwardedOperations.length).toBe(2);
-  expect(reexecuteOperation).not.toBeCalled();
+    it('is true when cached response is returned', async () => {
+      const [ops$, next, complete] = input;
+      const exchange = cacheExchange(exchangeArgs)(ops$);
+
+      const results$ = pipe(
+        exchange,
+        scan((acc, x) => [...acc, x], [] as OperationResult[]),
+        toPromise
+      );
+
+      publish(exchange);
+      next(queryOperation);
+      next(queryOperation);
+      complete();
+
+      const results = await results$;
+      expect(results[1].operation.context).toHaveProperty(
+        'meta.cacheOutcome',
+        'hit'
+      );
+    });
+  });
 });
 
-it('retriggers query operation when mutation occurs', () => {
-  const typename = 'ExampleType';
-  const resultCache = new Map([[123, queryResponse]]);
-  const operationCache = { [typename]: new Set([123]) };
-
-  afterMutation(resultCache, operationCache, exchangeArgs.client)({
-    ...mutationResponse,
-    data: {
-      todos: [
-        {
-          id: 1,
-          __typename: typename,
-        },
-      ],
-    },
+describe('on mutation', () => {
+  it('does not cache', () => {
+    response = mutationResponse;
+    const [ops$, next, complete] = input;
+    const exchange = cacheExchange(exchangeArgs)(ops$);
+
+    publish(exchange);
+    next(mutationOperation);
+    next(mutationOperation);
+    complete();
+    expect(forwardedOperations.length).toBe(2);
+    expect(reexecuteOperation).not.toBeCalled();
   });
 
-  expect(reexecuteOperation).toBeCalledTimes(1);
+  it('retriggers query operation', () => {
+    const typename = 'ExampleType';
+    const resultCache = new Map([[123, queryResponse]]);
+    const operationCache = { [typename]: new Set([123]) };
+
+    afterMutation(resultCache, operationCache, exchangeArgs.client)({
+      ...mutationResponse,
+      data: {
+        todos: [
+          {
+            id: 1,
+            __typename: typename,
+          },
+        ],
+      },
+    });
+
+    expect(reexecuteOperation).toBeCalledTimes(1);
+  });
 });
 
-it('forwards subscriptions', () => {
-  response = subscriptionResult;
-  const [ops$, next, complete] = input;
-  const exchange = cacheExchange(exchangeArgs)(ops$);
-
-  publish(exchange);
-  next(subscriptionOperation);
-  next(subscriptionOperation);
-  complete();
-  expect(forwardedOperations.length).toBe(2);
-  expect(reexecuteOperation).not.toBeCalled();
+describe('on subscription', () => {
+  it('forwards subscriptions', () => {
+    response = subscriptionResult;
+    const [ops$, next, complete] = input;
+    const exchange = cacheExchange(exchangeArgs)(ops$);
+
+    publish(exchange);
+    next(subscriptionOperation);
+    next(subscriptionOperation);
+    complete();
+    expect(forwardedOperations.length).toBe(2);
+    expect(reexecuteOperation).not.toBeCalled();
+  });
 });
 
-describe('undefined data', () => {
+// Empty query response implies the data propertys is undefined
+describe('on empty query response', () => {
   beforeEach(() => {
     response = undefinedQueryResponse;
     forwardedOperations = [];
@@ -136,7 +190,7 @@ describe('undefined data', () => {
     exchangeArgs = { forward, client };
   });
 
-  it('should not cache on undefined result', () => {
+  it('does not cache response', () => {
     const [ops$, next, complete] = input;
     const exchange = cacheExchange(exchangeArgs)(ops$);
 
diff --git a/src/exchanges/fetch.test.ts b/src/exchanges/fetch.test.ts
index b809ae2f..fc7d30f0 100755
--- a/src/exchanges/fetch.test.ts
+++ b/src/exchanges/fetch.test.ts
@@ -40,69 +40,114 @@ const exchangeArgs = {
   client: {} as Client,
 };
 
-it('returns response data from fetch', async () => {
-  fetch.mockResolvedValue({
-    status: 200,
-    json: jest.fn().mockResolvedValue(response),
+describe('on success', () => {
+  beforeEach(() => {
+    jest
+      .spyOn(Date, 'now')
+      .mockImplementationOnce(() => 100)
+      .mockImplementationOnce(() => 200);
+
+    fetch.mockResolvedValue({
+      status: 200,
+      json: jest.fn().mockResolvedValue(response),
+    });
   });
 
-  const fetchOptions = jest.fn().mockReturnValue({});
-
-  const data = await pipe(
-    fromValue({
-      ...queryOperation,
-      context: {
-        ...queryOperation.context,
-        fetchOptions,
-      },
-    }),
-    fetchExchange(exchangeArgs),
-    toPromise
-  );
-
-  expect(data).toMatchSnapshot();
-  expect(fetchOptions).toHaveBeenCalled();
+  it('returns response data', async () => {
+    const fetchOptions = jest.fn().mockReturnValue({});
+
+    const data = await pipe(
+      fromValue({
+        ...queryOperation,
+        context: {
+          ...queryOperation.context,
+          fetchOptions,
+        },
+      }),
+      fetchExchange(exchangeArgs),
+      toPromise
+    );
+
+    expect(data).toMatchSnapshot();
+    expect(fetchOptions).toHaveBeenCalled();
+  });
+
+  it('adds latency info to the context', async () => {
+    const data = await pipe(
+      fromValue({
+        ...queryOperation,
+        context: {
+          ...queryOperation.context,
+        },
+      }),
+      fetchExchange(exchangeArgs),
+      toPromise
+    );
+
+    expect(data.operation.context).toHaveProperty('meta.networkLatency', 100);
+  });
 });
 
-it('returns error data from fetch', async () => {
-  fetch.mockResolvedValue({
-    status: 400,
-    json: jest.fn().mockResolvedValue(response),
+describe('on error', () => {
+  beforeEach(() => {
+    jest
+      .spyOn(Date, 'now')
+      .mockImplementationOnce(() => 100)
+      .mockImplementationOnce(() => 200);
+
+    fetch.mockResolvedValue({
+      status: 400,
+      json: jest.fn().mockResolvedValue(response),
+    });
+  });
+
+  it('returns error data', async () => {
+    const data = await pipe(
+      fromValue(queryOperation),
+      fetchExchange(exchangeArgs),
+      toPromise
+    );
+
+    expect(data).toMatchSnapshot();
   });
 
-  const data = await pipe(
-    fromValue(queryOperation),
-    fetchExchange(exchangeArgs),
-    toPromise
-  );
+  it('returns error data', async () => {
+    const data = await pipe(
+      fromValue(queryOperation),
+      fetchExchange(exchangeArgs),
+      toPromise
+    );
 
-  expect(data).toMatchSnapshot();
+    expect(data.operation.context).toHaveProperty('meta.networkLatency', 100);
+  });
 });
 
-it('calls cancel when the Observable is cancelled', () => {
-  fetch.mockReturnValue(Promise.reject(abortError));
+describe('on teardown', () => {
+  it('aborts the outgoing request', () => {
+    fetch.mockReturnValue(Promise.reject(abortError));
 
-  const [unsubscribe] = pipe(
-    fromValue(queryOperation),
-    fetchExchange(exchangeArgs),
-    subscribe(fail)
-  );
+    const [unsubscribe] = pipe(
+      fromValue(queryOperation),
+      fetchExchange(exchangeArgs),
+      subscribe(fail)
+    );
 
-  unsubscribe();
-  expect(fetch).toHaveBeenCalledTimes(1);
-  expect(abort).toHaveBeenCalledTimes(1);
-});
+    unsubscribe();
+    expect(fetch).toHaveBeenCalledTimes(1);
+    expect(abort).toHaveBeenCalledTimes(1);
+  });
 
-it('should not start the operation when teardown gets passed', () => {
-  pipe(
-    fromValue({
-      ...queryOperation,
-      operationName: 'teardown' as OperationType,
-    }),
-    fetchExchange(exchangeArgs),
-    subscribe(fail)
-  );
-
-  expect(fetch).toHaveBeenCalledTimes(0);
-  expect(abort).toHaveBeenCalledTimes(0);
+  it('does not call the query', () => {
+    pipe(
+      fromValue({
+        ...queryOperation,
+        operationName: 'teardown' as OperationType,
+      }),
+      fetchExchange(exchangeArgs),
+      subscribe(fail)
+    );
+
+    expect(fetch).toHaveBeenCalledTimes(0);
+    expect(abort).toHaveBeenCalledTimes(0);
+  });
 });
diff --git a/src/hooks/useMutation.test.tsx b/src/hooks/useMutation.test.tsx
index 5cffa0db..321adf5a 100644
--- a/src/hooks/useMutation.test.tsx
+++ b/src/hooks/useMutation.test.tsx
@@ -112,7 +112,7 @@ describe('on execute', () => {
     act(() => {
       execute(vars);
     });
-    expect(client.executeMutation.mock.calls[0][1]).toHaveProperty('devtools', {
+    expect(client.executeMutation.mock.calls[0][1]).toHaveProperty('meta', {
       source: 'MutationUser',
     });
   });
diff --git a/src/hooks/useQuery.test.tsx b/src/hooks/useQuery.test.tsx
index eb5cc4d3..9fa87b27 100644
--- a/src/hooks/useQuery.test.tsx
+++ b/src/hooks/useQuery.test.tsx
@@ -95,7 +95,7 @@ describe('on initial useEffect', () => {
     expect(client.executeQuery).toBeCalledWith(
       expect.any(Object),
       expect.objectContaining({
-        devtools: {
+        meta: {
           source: 'QueryUser',
         },
       })
diff --git a/src/hooks/useSubscription.test.tsx b/src/hooks/useSubscription.test.tsx
index 3ab36f77..ca8efbf9 100644
--- a/src/hooks/useSubscription.test.tsx
+++ b/src/hooks/useSubscription.test.tsx
@@ -65,7 +65,7 @@ describe('on initial useEffect', () => {
     renderer.create(<SubscriptionUser q={query} />);
     expect(client.executeSubscription).toBeCalledWith(
       expect.any(Object),
-      expect.objectContaining({ devtools: { source: 'SubscriptionUser' } })
+      expect.objectContaining({ meta: { source: 'SubscriptionUser' } })
     );
   });
 });
diff --git a/src/utils/typenames.test.ts b/src/utils/typenames.test.ts
index e8b95a83..aadf1403 100755
--- a/src/utils/typenames.test.ts
+++ b/src/utils/typenames.test.ts
@@ -7,15 +7,37 @@ const formatTypeNames = (query: string) => {
 };
 
 describe('formatTypeNames', () => {
-  it('should add typenames to a query string', () => {
-    expect(formatTypeNames(`{ todos { id } }`)).toBe(`{
-  todos {
-    id
-    __typename
-  }
-  __typename
-}
-`);
+  it('adds typenames to a query string', () => {
+    expect(formatTypeNames(`{ todos { id } }`)).toMatchInlineSnapshot(`
+      "{
+        todos {
+          id
+          __typename
+        }
+        __typename
+      }
+      "
+    `);
+  });
+
+  it('does not duplicate typenames', () => {
+    expect(
+      formatTypeNames(`{ 
+      todos { 
+        id
+      }
+      __typename
+    }`)
+    ).toMatchInlineSnapshot(`
+            "{
+              todos {
+                id
+                __typename
+              }
+              __typename
+            }
+            "
+        `);
   });
 });