Reactコンポーネントのテストを書く時によく使用するコードまとめ

# はじめに
Reactコンポーネントのテスト作成する時のよくあるパターンをまとめてみました。

## react-selectコンポーネントのテスト
### react-select-eventの代わりに
````typescript
import userEvent from '@testing-library/user-event'
...
const select = (el: Element, text: string, user = userEvent) => user.type(`${text}{enter}`)
...
await select(screen.getByRole('combobox'), 'test')
````
## カスタムレンダー
今回はReduxストアー、Formikとreact-routerそれぞれ選択できるようにしてますが、
プロジェクトのニーズに合わせて、オプションを修正すれば様々なパータンをカバー出来ます。

### タイプ
````typescript
type RenderWithOpts<T> = {
  /* Redux設定 (ignoreDispathはストアーのdispatchファンクションを無効化にするオプション) */
  withStore?: { initialState: unknown; ignoreDispatch?: boolean } | boolean
  /* Formik設定 */
  withFormik?: Omit<FormikConfig<T>, 'onSubmit'>
  /* react-router設定 */
  withRouter?: boolean | Parameters<typeof createMemoryRouter>[1]
}
````
### コアーファンクション
````typescript
const wrapWith: <T extends object>(
  element: ReactElement,
  options: RenderWithOpts<T>,
) => {
  component: ReactElement
  onSubmit?: jest.Mock
  router?: ReturnType<typeof createMemoryRouter>
} & Omit<ReturnType<typeof includeStore>, 'store' | 'dispatchSpy'> &
  Partial<Pick<ReturnType<typeof includeStore>, 'store' | 'dispatchSpy'>> = (
  element,
  { withStore, withFormik: formikOptions, withRouter },
) => {
  const onSubmit = jest.fn()
  if (formikOptions) {
    element = (
        <Formik {...formikOptions} onSubmit={onSubmit}>
          {element}
        </Formik>
    )
  }
  let router
  if (withRouter) {
    router = createMemoryRouter(
      [
        {
          path: '*',
          element,
        },
      ],
      typeof withRouter === 'object' ? withRouter : undefined,
    )
    element = <RouterProvider router={router} />
  } else {
    router = undefined
  }
  const { component, ...store } = withStore
    ? includeStore(element, withStore)
    : { component: element }

  return {
    component,
    onSubmit,
    router,
    ...store,
  }
}
````

### Redux設定
`configureAppStore`は自分のストアー作成ファンクションになります。
`OnUnmount`はただテスト後、コンポーネントが処分されたら、ファンクションをコールするコンポーネントです。（なくても問題ないかと思います）
````typescript
const includeStore = (
  el: ReactElement,
  withStore: RenderWithOpts<unknown>['withStore'],
) => {
  const [store, unsubscribe] = configureAppStore({
    useLogger: false,
    initialState: typeof withStore === 'object' ? withStore.initialState : {},
  })
  const dispatchSpy = jest.spyOn(store, 'dispatch')
  if (typeof withStore === 'object' && withStore.ignoreDispatch) {
    dispatchSpy.mockImplementation(() => {})
  }
  return {
    store,
    dispatchSpy,
    component: (
      <Provider store={store}>
        {el}
        <OnUnmount fn={unsubscribe} />
      </Provider>
    ),
  }
}
````

### レンダーファンクション
testing-library/rectのrenderの上にwrapWithを使用すれば、カスタムレンダーは完成です！
````typescript
const customRender: <T extends object>(
  ui: ReactElement,
  renderOptions?: Omit<RenderOptions, 'queries'> & RenderWithOpts<object>,
) => ReturnType<typeof render> &
  Omit<ReturnType<typeof wrapWith<T>>, 'component'> = (
  ui,
  renderOptions = {},
) => {
  const { component, ...rest } = wrapWith(ui, renderOptions)
  return {
    ...render(component, {
      queries: allQueries,
      ...renderOptions,
    }),
    ...rest,
  }
}
````

### プラスアルファ（フックレンダー）
先程のrenderのrenderHook版：
````typescript
const renderHookWith: <T extends object, Result, Props>(
  hook: Parameters<typeof renderHook<Result, Props>>[0],
  renderOptions: Partial<Parameters<typeof renderHook<Result, Props>>[1]> &
    RenderWithOpts<T>,
) => ReturnType<typeof renderHook<Result, Props>> &
  Omit<ReturnType<typeof wrapWith<T>>, 'component'> = (
  hook,
  renderOptions = {},
) => {
  let result
  return {
    ...renderHook(hook, {
      ...renderOptions,
      wrapper: ({ children }) => {
        result = wrapWith(children, renderOptions)
        return result.component
      },
    }),
    ...omit(result, 'component'),
  }
}
````

## ファイルインプットテスト
````typescript
const fileInput = async (el: HTMLElement, str: string, user = userEvent, fileName = 'values.json', fileType = 'application/JSON') => {
  const blob = new Blob([str])
  const file = new File([blob], fileName, {
    type: fileType,
  })
  File.prototype.text = jest.fn().mockResolvedValueOnce(str)
  await user.upload(el, file)
}
...
await fileInput(screen.getByTestId('testid'), 'testValue')
````

## XHRモック
````typescript
万が一XHRを使用してるコードをテストしたい時に：
class XHRMock {
  listeners: Record<string, ((_: Record<string, unknown>) => void)[]> = {}

  headers: Record<string, string> = {}

  open = jest.fn()

  send = jest.fn()

  setRequestHeader = (key: string, val: string) => {
    this.headers[key] = val
  }

  getAllResponseHeaders = () =>
    Object.entries(this.headers)
      .map(([key, val]) => `${key}Return: ${JSON.stringify(val)}`)
      .join('\r\n')

  readyState = 4

  status = 200

  response = ''

  addEventListener = (
    type: string,
    handler: (_: Record<string, unknown>) => void,
  ) => {
    this.listeners[type] = [...(this.listeners[type] || []), handler]
  }

  upload = {
    addEventListener: this.addEventListener,
  }

  dispatchEvent = (ev: { type: string } & Record<string, unknown>) => {
    (this.listeners[ev.type] || []).forEach(handler => handler(ev))
  }
}

export const createXHRMock = () => new XHRMock()
...
const xhrMock = createXHRMock()
  jest
    .spyOn(window, 'XMLHttpRequest')
    .mockImplementation(() => xhrMock as unknown as XMLHttpRequest)
...
xhrMock.dispatchEvent({ type: 'progress', total: 120, loaded: 60 })
...
xhrMock.dispatchEvent({ type: 'loadend' })
````
## BroadcastChannelモック
````typescript
export const createBroadcastChannelMockClass = () =>
  class BroadcastChannelMock {
    closed = false

    static instances: { [name: string]: BroadcastChannelMock } = {}

    constructor(name: string) {
      BroadcastChannelMock.instances[name] = this
    }

    public close() {
      this.closed = true
    }

    onmessage(_: { data: unknown }) {}

    public postMessage(data: unknown) {
      this.onmessage({ data })
    }
  }

type TeardownFn = () => void | Promise<void | unknown>

export const installBroadcastChannelMock = ({
  additionalSetup,
}: {
  additionalSetup?: () => Promise<TeardownFn> | TeardownFn | undefined
} = {}) => {
  const org = global.BroadcastChannel
  const mock = createBroadcastChannelMockClass()
  let additionalTeardown: TeardownFn | undefined

  beforeAll(async () => {
    global.BroadcastChannel = mock as unknown as typeof org
    const teardown = additionalSetup?.()
    if (teardown instanceof Promise) {
      additionalTeardown = await teardown
    } else {
      additionalTeardown = teardown
    }
  })

  afterAll(() => {
    global.BroadcastChannel = org
    return additionalTeardown?.()
  })

  return mock
}

````


# 最後に
こちらのコードが参考になれたら幸いです。
皆さんの方はどうでしょうか？
便利そうなテストコードがあれば是非共有お願いします。