reactTestingLibrary.tsx 7.2 KB

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