Skip to content

Commit a342e26

Browse files
feat(rr6): Add an option to disableRouterMocks (#78715)
Allows us to write tests that actually use the routers functionality
1 parent 4d79fc4 commit a342e26

File tree

9 files changed

+80
-70
lines changed

9 files changed

+80
-70
lines changed

static/app/utils/useLocation.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {useMemo} from 'react';
22
import {useLocation as useReactRouter6Location} from 'react-router-dom';
33
import type {Location, Query} from 'history';
44

5-
import {NODE_ENV} from 'sentry/constants';
65
import {useRouteContext} from 'sentry/utils/useRouteContext';
76

87
import {location6ToLocation3} from './reactRouter6Compat/location';
@@ -14,11 +13,10 @@ type DefaultQuery<T = string> = {
1413
export function useLocation<Q extends Query = DefaultQuery>(): Location<Q> {
1514
// When running in test mode we still read from the legacy route context to
1615
// keep test compatability while we fully migrate to react router 6
17-
const useReactRouter6 = NODE_ENV !== 'test';
16+
const legacyRouterContext = useRouteContext();
1817

19-
if (!useReactRouter6) {
20-
// biome-ignore lint/correctness/useHookAtTopLevel: react-router 6 migration
21-
return useRouteContext().location;
18+
if (legacyRouterContext) {
19+
return legacyRouterContext.location;
2220
}
2321

2422
// biome-ignore lint/correctness/useHookAtTopLevel: react-router 6 migration

static/app/utils/useNavigate.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {useCallback, useEffect, useRef} from 'react';
22
import {useNavigate as useReactRouter6Navigate} from 'react-router-dom';
33
import type {LocationDescriptor} from 'history';
44

5-
import {NODE_ENV} from 'sentry/constants';
65
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
76

87
import {locationDescriptorToTo} from './reactRouter6Compat/location';
@@ -27,9 +26,9 @@ interface ReactRouter3Navigate {
2726
export function useNavigate(): ReactRouter3Navigate {
2827
// When running in test mode we still read from the legacy route context to
2928
// keep test compatability while we fully migrate to react router 6
30-
const useReactRouter6 = NODE_ENV !== 'test';
29+
const legacyRouterContext = useRouteContext();
3130

32-
if (useReactRouter6) {
31+
if (!legacyRouterContext) {
3332
// biome-ignore lint/correctness/useHookAtTopLevel: react-router-6 migration
3433
const router6Navigate = useReactRouter6Navigate();
3534

@@ -51,8 +50,7 @@ export function useNavigate(): ReactRouter3Navigate {
5150
// XXX(epurkihser): We are using react-router 3 here, to avoid recursive
5251
// dependencies we just use the useRouteContext instead of useRouter here
5352

54-
// biome-ignore lint/correctness/useHookAtTopLevel: react-router-6 migration
55-
const {router} = useRouteContext();
53+
const {router} = legacyRouterContext;
5654

5755
// biome-ignore lint/correctness/useHookAtTopLevel: react-router-6 migration
5856
const hasMountedRef = useRef(false);

static/app/utils/useParams.spec.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe('useParams', () => {
8989
let useParamsValue;
9090

9191
function Component() {
92-
const {params} = useRouteContext();
92+
const {params} = useRouteContext()!;
9393
originalParams = params;
9494
useParamsValue = useParams();
9595
return (
@@ -127,7 +127,7 @@ describe('useParams', () => {
127127
let useParamsValue;
128128

129129
function Component() {
130-
const {params} = useRouteContext();
130+
const {params} = useRouteContext()!;
131131
originalParams = params;
132132
useParamsValue = useParams();
133133
return (

static/app/utils/useParams.tsx

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import {useMemo} from 'react';
22
import {useParams as useReactRouter6Params} from 'react-router-dom';
33

4-
import {CUSTOMER_DOMAIN, NODE_ENV, USING_CUSTOMER_DOMAIN} from 'sentry/constants';
5-
import {useRouteContext} from 'sentry/utils/useRouteContext';
4+
import {CUSTOMER_DOMAIN, USING_CUSTOMER_DOMAIN} from 'sentry/constants';
5+
6+
import {useRouteContext} from './useRouteContext';
67

78
export function useParams<P = Record<string, string>>(): P {
89
// When running in test mode we still read from the legacy route context to
910
// keep test compatability while we fully migrate to react router 6
10-
const useReactRouter6 = NODE_ENV !== 'test';
11+
const legacyRouterContext = useRouteContext();
1112

1213
let contextParams: any;
1314

14-
if (useReactRouter6) {
15+
if (!legacyRouterContext) {
1516
// biome-ignore lint/correctness/useHookAtTopLevel: react-router 6 migration
1617
contextParams = useReactRouter6Params();
1718
} else {
18-
// biome-ignore lint/correctness/useHookAtTopLevel: react-router 6 migration
19-
contextParams = useRouteContext().params;
19+
contextParams = legacyRouterContext.params;
2020
}
2121

2222
// Memoize params as mutating for customer domains causes other hooks

static/app/utils/useRouteContext.tsx

+1-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,5 @@ import {useContext} from 'react';
33
import {RouteContext} from 'sentry/views/routeContext';
44

55
export function useRouteContext() {
6-
const route = useContext(RouteContext);
7-
if (route === null) {
8-
throw new Error(`useRouteContext called outside of routes provider`);
9-
}
10-
return route;
6+
return useContext(RouteContext);
117
}

static/app/utils/useRouter.tsx

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import {useMemo} from 'react';
22
import type {LocationDescriptor} from 'history';
33

4-
import {NODE_ENV} from 'sentry/constants';
54
import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
6-
import {useRouteContext} from 'sentry/utils/useRouteContext';
75

86
import {useLocation} from './useLocation';
97
import {useNavigate} from './useNavigate';
108
import {useParams} from './useParams';
9+
import {useRouteContext} from './useRouteContext';
1110
import {useRoutes} from './useRoutes';
1211

1312
/**
@@ -19,11 +18,10 @@ import {useRoutes} from './useRoutes';
1918
function useRouter(): InjectedRouter<any, any> {
2019
// When running in test mode we still read from the legacy route context to
2120
// keep test compatability while we fully migrate to react router 6
22-
const useReactRouter6 = NODE_ENV !== 'test';
21+
const legacyRouterContext = useRouteContext();
2322

24-
if (!useReactRouter6) {
25-
// biome-ignore lint/correctness/useHookAtTopLevel: react-router 6 migration
26-
return useRouteContext().router;
23+
if (legacyRouterContext) {
24+
return legacyRouterContext.router;
2725
}
2826

2927
// biome-ignore lint/correctness/useHookAtTopLevel: react-router 6 migration

static/app/utils/useRoutes.tsx

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
import {useMemo} from 'react';
22
import {useMatches} from 'react-router-dom';
33

4-
import {NODE_ENV} from 'sentry/constants';
54
import type {PlainRoute} from 'sentry/types/legacyReactRouter';
6-
import {useRouteContext} from 'sentry/utils/useRouteContext';
5+
6+
import {useRouteContext} from './useRouteContext';
77

88
export function useRoutes(): PlainRoute<any>[] {
99
// When running in test mode we still read from the legacy route context to
1010
// keep test compatability while we fully migrate to react router 6
11-
const useReactRouter6 = NODE_ENV !== 'test';
11+
const legacyRouterContext = useRouteContext();
1212

13-
if (!useReactRouter6) {
14-
// biome-ignore lint/correctness/useHookAtTopLevel: react-router-6 migration
15-
return useRouteContext().routes;
13+
if (legacyRouterContext) {
14+
return legacyRouterContext.routes;
1615
}
1716

1817
// biome-ignore lint/correctness/useHookAtTopLevel: react-router-6 migration

static/app/views/routeContext.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@ import {createContext} from 'react';
22

33
import type {RouteContextInterface} from 'sentry/types/legacyReactRouter';
44

5-
// TODO(nisanthan): Better types. Context will be the `props` arg from the RouterProps render method. This is typed as `any` by react-router
5+
/**
6+
* This is a legacy context that is primarily used in tests currently to allow
7+
* for mocking the use{Location,Navigate,Routes,Params} hooks
8+
*
9+
* DO NOT use this outside of tests!
10+
*/
611
export const RouteContext = createContext<RouteContextInterface | null>(null);

tests/js/sentry-test/reactTestingLibrary.tsx

+50-34
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,44 @@ import {instrumentUserEvent} from '../instrumentedEnv/userEventIntegration';
2222
import {initializeOrg} from './initializeOrg';
2323

2424
interface ProviderOptions {
25+
/**
26+
* Do not shim the router use{Routes,Router,Navigate,Location} functions, and
27+
* instead allow them to work as normal, rendering inside of a memory router.
28+
*
29+
* Wehn enabling this passing a `router` object *will do nothing*!
30+
*/
31+
disableRouterMocks?: boolean;
2532
/**
2633
* Sets the OrganizationContext. You may pass null to provide no organization
2734
*/
2835
organization?: Partial<Organization> | null;
2936
/**
30-
* Sets the RouterContext
37+
* Sets the RouterContext.
3138
*/
3239
router?: Partial<InjectedRouter>;
3340
}
3441

3542
interface Options extends ProviderOptions, rtl.RenderOptions {}
3643

37-
function makeAllTheProviders(providers: ProviderOptions) {
44+
function makeAllTheProviders(options: ProviderOptions) {
3845
const {organization, router} = initializeOrg({
39-
organization: providers.organization === null ? undefined : providers.organization,
40-
router: providers.router,
46+
organization: options.organization === null ? undefined : options.organization,
47+
router: options.router,
4148
});
4249

4350
// In some cases we may want to not provide an organization at all
44-
const optionalOrganization = providers.organization === null ? null : organization;
51+
const optionalOrganization = options.organization === null ? null : organization;
4552

4653
return function ({children}: {children?: React.ReactNode}) {
4754
const content = (
55+
<OrganizationContext.Provider value={optionalOrganization}>
56+
<GlobalDrawer>{children}</GlobalDrawer>
57+
</OrganizationContext.Provider>
58+
);
59+
60+
const wrappedContent = options.disableRouterMocks ? (
61+
content
62+
) : (
4863
<RouteContext.Provider
4964
value={{
5065
router,
@@ -53,41 +68,41 @@ function makeAllTheProviders(providers: ProviderOptions) {
5368
routes: router.routes,
5469
}}
5570
>
56-
<OrganizationContext.Provider value={optionalOrganization}>
57-
<GlobalDrawer>{children}</GlobalDrawer>
58-
</OrganizationContext.Provider>
71+
{content}
5972
</RouteContext.Provider>
6073
);
6174

75+
const history = createMemoryHistory();
76+
6277
// Inject legacy react-router 3 style router mocked navigation functions
6378
// into the memory history used in react router 6
6479
//
6580
// TODO(epurkhiser): In a world without react-router 3 we should figure out
6681
// how to write our tests in a simpler way without all these shims
67-
68-
const history = createMemoryHistory();
69-
Object.defineProperty(history, 'location', {get: () => router.location});
70-
history.replace = router.replace;
71-
history.push = (path: any) => {
72-
if (typeof path === 'object' && path.search) {
73-
path.query = qs.parse(path.search);
74-
delete path.search;
75-
delete path.hash;
76-
delete path.state;
77-
delete path.key;
78-
}
79-
80-
// XXX(epurkhiser): This is a hack for react-router 3 to 6. react-router
81-
// 6 will not convert objects into strings before pushing. We can detect
82-
// this by looking for an empty hash, which we normally do not set for
83-
// our browserHistory.push calls
84-
if (typeof path === 'object' && path.hash === '') {
85-
const queryString = path.query ? qs.stringify(path.query) : null;
86-
path = `${path.pathname}${queryString ? `?${queryString}` : ''}`;
87-
}
88-
89-
router.push(path);
90-
};
82+
if (!options.disableRouterMocks) {
83+
Object.defineProperty(history, 'location', {get: () => router.location});
84+
history.replace = router.replace;
85+
history.push = (path: any) => {
86+
if (typeof path === 'object' && path.search) {
87+
path.query = qs.parse(path.search);
88+
delete path.search;
89+
delete path.hash;
90+
delete path.state;
91+
delete path.key;
92+
}
93+
94+
// XXX(epurkhiser): This is a hack for react-router 3 to 6. react-router
95+
// 6 will not convert objects into strings before pushing. We can detect
96+
// this by looking for an empty hash, which we normally do not set for
97+
// our browserHistory.push calls
98+
if (typeof path === 'object' && path.hash === '') {
99+
const queryString = path.query ? qs.stringify(path.query) : null;
100+
path = `${path.pathname}${queryString ? `?${queryString}` : ''}`;
101+
}
102+
103+
router.push(path);
104+
};
105+
}
91106

92107
// By default react-router 6 catches exceptions and displays the stack
93108
// trace. For tests we want them to bubble out
@@ -98,7 +113,7 @@ function makeAllTheProviders(providers: ProviderOptions) {
98113
const routes: RouteObject[] = [
99114
{
100115
path: '*',
101-
element: content,
116+
element: wrappedContent,
102117
errorElement: <ErrorBoundary />,
103118
},
104119
];
@@ -132,11 +147,12 @@ function makeAllTheProviders(providers: ProviderOptions) {
132147
*/
133148
function render(
134149
ui: React.ReactElement,
135-
{router, organization, ...rtlOptions}: Options = {}
150+
{router, organization, disableRouterMocks, ...rtlOptions}: Options = {}
136151
) {
137152
const AllTheProviders = makeAllTheProviders({
138153
organization,
139154
router,
155+
disableRouterMocks,
140156
});
141157

142158
return rtl.render(ui, {wrapper: AllTheProviders, ...rtlOptions});

0 commit comments

Comments
 (0)