# renderDataHook()

`renderDataHook()` is useful to test hooks that rely on the `Reactive Data Client`. It mirrors [@testing-library/react-hooks](https://github.com/testing-library/react-hooks-testing-library)'s [renderHook()](https://react-hooks-testing-library.com/reference/api#renderhook-options) but does so with a `<Suspense/>` boundary
as well as in a `<Provider />` context.

> **Note**
>
> `renderDataHook()` creates a Provider context with new manager instances. This means each call
> to `renderDataHook()` will result in a completely fresh cache state as well as manager state.

<details>

<summary>Type</summary>

```typescript
type RenderDataHook = {
  <P, R, T = any>(
    callback: (props: P) => R,
    options?: {
      initialProps?: P;
      initialFixtures?: Fixture[];
      resolverFixtures?: (Fixture | Interceptor<T>)[];
      getInitialInterceptorData?: () => T;
      wrapper?: React.ComponentType;
    },
  ): {
    rerender: (props?: Props) => void;
    result: {
      current: Result;
      error?: Error;
    };
    unmount: () => void;
    controller: Controller;
    cleanup(): void;
    allSettled(): Promise<unknown>;
    /* @deprecated */
    waitForNextUpdate: (options?: waitForOptions) => Promise<void>;
    waitFor<T>(
      callback: () => Promise<T> | T,
      options?: waitForOptions,
    ): Promise<T>;
  };
  /** cleanup is automatic; only needed for ordering (e.g., before jest.useRealTimers()) */
  cleanup(): void;
  allSettled(): Promise<unknown>;
};
```

</details>

## Usage

```typescript
import { useSuspense } from '@data-client/react';
import { renderDataHook } from '@data-client/test';
import { Article, ArticleResource } from './resources/Article';

const response = {
  id: 5,
  title: 'hi ho',
  content: 'whatever',
  tags: ['a', 'best', 'react'],
};

it('useSuspense() should render the response', async () => {
  const { result, waitFor } = renderDataHook(
    () => {
      return useSuspense(ArticleResource.get, { id: 5 });
    },
    {
      initialFixtures: [
        {
          endpoint: ArticleResource.get,
          args: [{ id: 5 }],
          response,
        },
      ],
    },
  );
  expect(result.current instanceof Article).toBe(true);
  expect(result.current.title).toBe(response.title);
});
```

## Arguments

### callback

Hook to run inside React. Return value will become available in [result.current](#result)

### options.initialFixtures

Can be used to prime the cache if test expects cache values to already be filled. Takes an
[array of fixtures](https://dataclient.io/docs/api/Fixtures.md)

This has the same effect as initializing [\<DataProvider />](https://dataclient.io/docs/api/DataProvider.md) with [mockInitialState()](https://dataclient.io/docs/api/mockInitialState.md)

### options.resolverFixtures

These [fixtures or interceptors](https://dataclient.io/docs/api/Fixtures.md) are used to resolve any new requests. This is most useful for mocking imperative fetches like mutations, but can also allow testing suspending states or transitions.

Works by adding [MockResolver](https://dataclient.io/docs/api/MockResolver.md) as a wrapper.

### options.getInitialInterceptorData

Function that initializes the `this` attribute for all interceptors.

### options.initialProps

The initial values to pass to the callback function

### options.wrapper

Pass a React Component as the wrapper option to have it rendered around the inner element

## Returns

### controller

[Controller](https://dataclient.io/docs/api/Controller.md) to dispatch imperative effects

```ts
import { act } from '@testing-library/react';
import { useSuspense } from '@data-client/react';
import { renderDataHook } from '@data-client/test';
import { Todo, TodoResource } from './resources/Todo';

it('should update', async () => {
  const id = 5;
  const payload = { title: 'first item', id, completed: false };
  const { result, controller } = renderDataHook(
    () => {
      return useSuspense(TodoResource.getList);
    },
    {
      initialFixtures: [
        {
          endpoint: TodoResource.getList,
          args: [],
          response: [payload],
        },
      ],
      resolverFixtures: [
        {
          endpoint: TodoResource.update,
          response: ({ id }, body) => ({ ...body, id }),
        },
      ],
    },
  );
  expect(result.current).toEqual([Todo.fromJS(payload)]);
  await act(async () => {
    await controller.fetch(
      TodoResource.update,
      { id },
      { title: 'updated title' },
    );
  });
  expect(result.current[0].title).toBe('updated title');
});
```

### cleanup()

Cleans up all managers used in this render.
This is especially important when mocking timers, as Reactive Data Client's internals rely on real timers to
avoid race conditions.

Cleanup runs automatically after each test via a module-level `afterEach` hook (similar to `@testing-library/react`).
Manual calls are only needed when you must control cleanup ordering within a test body -- for example,
cleaning up before switching from fake timers to real timers:

```ts
it('should handle polling', async () => {
  jest.useFakeTimers();
  const { result } = renderDataHook(/* ... */);
  // ... assertions ...
  renderDataHook.cleanup(); // must run while fake timers are still active
  jest.useRealTimers();
});
```

### allSettled()

Returns a promise that resolves once all inflight requests are completed.

Also available on the return value of each `renderDataHook()` call.

### result

- `current` (`any`) - the return value of the `callback` function
- `error` (`Error`) - the error that was thrown if the `callback` function threw an error during rendering

### waitFor

Returns a `Promise` that resolves if the provided callback executes without exception and returns a truthy or undefined value. It is safe to use the result of renderDataHook in the callback to perform assertion or to test values.

### waitForNextUpdate

> **Warning: Deprecated**
>
> Use waitFor instead

Returns a `Promise` that resolves the next time the hook renders, commonly when state is updated as the result of a asynchronous action.

### rerender

(`function([newProps])`) - function to rerender the test component including any hooks called in the `callback` function. If `newProps` are passed, the will replace the `initialProps` passed the the `callback` function for future renders.

### unmount

(`function()`) - function to unmount the test component, commonly used to trigger cleanup effects for `useEffect` hooks.

## Examples

```typescript
import { useSuspense } from '@data-client/react';
import { renderDataHook } from '@data-client/test';
import { Article, ArticleResource } from './resources/Article';

const response = {
  id: 5,
  title: 'hi ho',
  content: 'whatever',
  tags: ['a', 'best', 'react'],
};

it('should resolve useSuspense()', async () => {
  const { result, waitFor, controller } = renderDataHook(
    () => {
      return useSuspense(ArticleResource.get, response);
    },
    {
      resolverFixtures: [
        {
          endpoint: ArticleResource.get,
          response: ({ id }) => ({ ...response, id }),
        },
        {
          endpoint: ArticleResource.partialUpdate,
          response: ({ id }, body) => ({ ...body, id }),
        },
      ],
    },
  );
  // this indicates suspense
  expect(result.current).toBeUndefined();
  await waitFor(() => expect(result.current).toBeDefined());
  expect(result.current instanceof Article).toBe(true);
  expect(result.current.title).toBe(response.title);
  await controller.fetch(
    ArticleResource.partialUpdate,
    { id: response.id },
    { title: 'updated title' },
  );
  expect(result.current.title).toBe('updated title');
});
```
