reactTestingLibrary.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import {type RouteObject, RouterProvider, useRouteError} from 'react-router-dom';
  2. import {cache} from '@emotion/css'; // eslint-disable-line @emotion/no-vanilla
  3. import {CacheProvider, ThemeProvider} from '@emotion/react';
  4. import {createMemoryHistory, createRouter} from '@remix-run/router';
  5. import * as rtl from '@testing-library/react'; // eslint-disable-line no-restricted-imports
  6. import userEvent from '@testing-library/user-event'; // eslint-disable-line no-restricted-imports
  7. import * as qs from 'query-string';
  8. import {makeTestQueryClient} from 'sentry-test/queryClient';
  9. import {GlobalDrawer} from 'sentry/components/globalDrawer';
  10. import GlobalModal from 'sentry/components/globalModal';
  11. import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
  12. import type {Organization} from 'sentry/types/organization';
  13. import {QueryClientProvider} from 'sentry/utils/queryClient';
  14. import {lightTheme} from 'sentry/utils/theme';
  15. import {OrganizationContext} from 'sentry/views/organizationContext';
  16. import {RouteContext} from 'sentry/views/routeContext';
  17. import {instrumentUserEvent} from '../instrumentedEnv/userEventIntegration';
  18. import {initializeOrg} from './initializeOrg';
  19. interface ProviderOptions {
  20. /**
  21. * Do not shim the router use{Routes,Router,Navigate,Location} functions, and
  22. * instead allow them to work as normal, rendering inside of a memory router.
  23. *
  24. * Wehn enabling this passing a `router` object *will do nothing*!
  25. */
  26. disableRouterMocks?: boolean;
  27. /**
  28. * Sets the OrganizationContext. You may pass null to provide no organization
  29. */
  30. organization?: Partial<Organization> | null;
  31. /**
  32. * Sets the RouterContext.
  33. */
  34. router?: Partial<InjectedRouter>;
  35. }
  36. interface Options extends ProviderOptions, rtl.RenderOptions {}
  37. function makeAllTheProviders(options: ProviderOptions) {
  38. const {organization, router} = initializeOrg({
  39. organization: options.organization === null ? undefined : options.organization,
  40. router: options.router,
  41. });
  42. // In some cases we may want to not provide an organization at all
  43. const optionalOrganization = options.organization === null ? null : organization;
  44. return function ({children}: {children?: React.ReactNode}) {
  45. const content = (
  46. <OrganizationContext.Provider value={optionalOrganization}>
  47. <GlobalDrawer>{children}</GlobalDrawer>
  48. </OrganizationContext.Provider>
  49. );
  50. const wrappedContent = options.disableRouterMocks ? (
  51. content
  52. ) : (
  53. <RouteContext.Provider
  54. value={{
  55. router,
  56. location: router.location,
  57. params: router.params,
  58. routes: router.routes,
  59. }}
  60. >
  61. {content}
  62. </RouteContext.Provider>
  63. );
  64. const history = createMemoryHistory();
  65. // Inject legacy react-router 3 style router mocked navigation functions
  66. // into the memory history used in react router 6
  67. //
  68. // TODO(epurkhiser): In a world without react-router 3 we should figure out
  69. // how to write our tests in a simpler way without all these shims
  70. if (!options.disableRouterMocks) {
  71. Object.defineProperty(history, 'location', {get: () => router.location});
  72. history.replace = router.replace;
  73. history.push = (path: any) => {
  74. if (typeof path === 'object' && path.search) {
  75. path.query = qs.parse(path.search);
  76. delete path.search;
  77. delete path.hash;
  78. delete path.state;
  79. delete path.key;
  80. }
  81. // XXX(epurkhiser): This is a hack for react-router 3 to 6. react-router
  82. // 6 will not convert objects into strings before pushing. We can detect
  83. // this by looking for an empty hash, which we normally do not set for
  84. // our browserHistory.push calls
  85. if (typeof path === 'object' && path.hash === '') {
  86. const queryString = path.query ? qs.stringify(path.query) : null;
  87. path = `${path.pathname}${queryString ? `?${queryString}` : ''}`;
  88. }
  89. router.push(path);
  90. };
  91. }
  92. // By default react-router 6 catches exceptions and displays the stack
  93. // trace. For tests we want them to bubble out
  94. function ErrorBoundary(): React.ReactNode {
  95. throw useRouteError();
  96. }
  97. const routes: RouteObject[] = [
  98. {
  99. path: '*',
  100. element: wrappedContent,
  101. errorElement: <ErrorBoundary />,
  102. },
  103. ];
  104. const memoryRouter = createRouter({
  105. future: {v7_prependBasename: true},
  106. history,
  107. routes,
  108. }).initialize();
  109. return (
  110. <CacheProvider value={{...cache, compat: true}}>
  111. <ThemeProvider theme={lightTheme}>
  112. <QueryClientProvider client={makeTestQueryClient()}>
  113. <RouterProvider router={memoryRouter} />
  114. </QueryClientProvider>
  115. </ThemeProvider>
  116. </CacheProvider>
  117. );
  118. };
  119. }
  120. /**
  121. * Try avoiding unnecessary context and just mount your component. If it works,
  122. * then you dont need anything else.
  123. *
  124. * render(<TestedComponent />);
  125. *
  126. * If your component requires additional context you can pass it in the
  127. * options.
  128. */
  129. function render(
  130. ui: React.ReactElement,
  131. {router, organization, disableRouterMocks, ...rtlOptions}: Options = {}
  132. ) {
  133. const AllTheProviders = makeAllTheProviders({
  134. organization,
  135. router,
  136. disableRouterMocks,
  137. });
  138. return rtl.render(ui, {wrapper: AllTheProviders, ...rtlOptions});
  139. }
  140. /**
  141. * @deprecated
  142. * Use userEvent over fireEvent where possible.
  143. * More details: https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-using-testing-libraryuser-event
  144. */
  145. const fireEvent = rtl.fireEvent;
  146. function renderGlobalModal(options?: Options) {
  147. const result = render(<GlobalModal />, options);
  148. /**
  149. * Helper that waits for the modal to be removed from the DOM. You may need to
  150. * wait for the modal to be removed to avoid any act warnings.
  151. */
  152. function waitForModalToHide() {
  153. return rtl.waitFor(() => {
  154. expect(rtl.screen.queryByRole('dialog')).not.toBeInTheDocument();
  155. });
  156. }
  157. return {...result, waitForModalToHide};
  158. }
  159. /**
  160. * Helper that waits for the drawer to be hidden from the DOM. You may need to
  161. * wait for the drawer to be removed to avoid any act warnings.
  162. */
  163. function waitForDrawerToHide(ariaLabel: string) {
  164. return rtl.waitFor(() => {
  165. expect(
  166. rtl.screen.queryByRole('complementary', {name: ariaLabel})
  167. ).not.toBeInTheDocument();
  168. });
  169. }
  170. /**
  171. * This cannot be implemented as a Sentry Integration because Jest creates an
  172. * isolated environment for each test suite. This means that if we were to apply
  173. * the monkey patching ahead of time, it would be shadowed by Jest.
  174. */
  175. instrumentUserEvent();
  176. // eslint-disable-next-line no-restricted-imports, import/export
  177. export * from '@testing-library/react';
  178. // eslint-disable-next-line import/export
  179. export {
  180. render,
  181. renderGlobalModal,
  182. userEvent,
  183. fireEvent,
  184. waitForDrawerToHide,
  185. makeAllTheProviders,
  186. };