go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/build/legacy/build_page/build_default_tab.test.tsx (about) 1 // Copyright 2023 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { cleanup, render } from '@testing-library/react'; 16 import { applySnapshot, destroy, Instance } from 'mobx-state-tree'; 17 import type { NavigateFunction } from 'react-router-dom'; 18 import * as reactRouterDom from 'react-router-dom'; 19 20 import { Store, StoreProvider } from '@/common/store'; 21 22 import { BuildDefaultTab } from './build_default_tab'; 23 import { BuildPageTab } from './common'; 24 25 jest.mock('react-router-dom', () => { 26 return createSelectiveMockFromModule<typeof import('react-router-dom')>( 27 'react-router-dom', 28 ['useNavigate'], 29 ); 30 }); 31 32 describe('BuildDefaultTab', () => { 33 let store: Instance<typeof Store>; 34 let useNavigateSpy: jest.MockedFunction< 35 () => jest.MockedFunction<NavigateFunction> 36 >; 37 38 beforeEach(() => { 39 jest.useFakeTimers(); 40 const navigateSpies = new Map< 41 NavigateFunction, 42 jest.MockedFunction<NavigateFunction> 43 >(); 44 useNavigateSpy = jest 45 .mocked( 46 // We will return a mocked `navigate` function so we can intercept the 47 // `navigate` calls. 48 reactRouterDom.useNavigate as () => jest.MockedFunction<NavigateFunction>, 49 ) 50 .mockImplementation(() => { 51 const navigate = ( 52 jest.requireActual('react-router-dom') as typeof reactRouterDom 53 ).useNavigate(); 54 // Return the same mock reference if the reference to `navigate` is the 55 // same. This is to ensure the dependency checks having the same result. 56 const navigateSpy = 57 navigateSpies.get(navigate) || 58 (jest.fn( 59 navigate, 60 // `jest.fn` isn't smart enough to infer the function type when 61 // mocking an overloaded function. Use manual casting instead. 62 ) as unknown as jest.MockedFunction<NavigateFunction>); 63 navigateSpies.set(navigate, navigateSpy); 64 return navigateSpy; 65 }); 66 store = Store.create({ userConfig: { build: { defaultTab: 'overview' } } }); 67 }); 68 69 afterEach(() => { 70 cleanup(); 71 destroy(store); 72 jest.useRealTimers(); 73 useNavigateSpy.mockRestore(); 74 }); 75 76 test('should redirect to the default tab', async () => { 77 const router = reactRouterDom.createMemoryRouter( 78 [ 79 { 80 path: 'path/prefix', 81 children: [ 82 { index: true, element: <BuildDefaultTab /> }, 83 { path: 'overview', element: <></> }, 84 ], 85 }, 86 ], 87 { initialEntries: ['/path/prefix?param#hash'] }, 88 ); 89 90 render( 91 <StoreProvider value={store}> 92 <reactRouterDom.RouterProvider router={router} /> 93 </StoreProvider>, 94 ); 95 96 expect(useNavigateSpy).toHaveBeenCalledTimes(1); 97 const useNavigateSpyResult = useNavigateSpy.mock.results[0]; 98 expect(useNavigateSpyResult.type).toEqual('return'); 99 // Won't happen. Useful for type inference. 100 if (useNavigateSpyResult.type !== 'return') { 101 throw new Error('unreachable'); 102 } 103 const navigateSpy = useNavigateSpyResult.value; 104 expect(navigateSpy).toHaveBeenCalledTimes(1); 105 expect(navigateSpy.mock.calls[0]).toMatchObject([ 106 // The type definition for `.toMatchObject` is incomplete. Cast to any to 107 // make TSC happy. 108 // eslint-disable-next-line @typescript-eslint/no-explicit-any 109 '/path/prefix/overview?param#hash' as any, 110 { replace: true }, 111 ]); 112 }); 113 114 test("should work with '/' suffix", async () => { 115 const router = reactRouterDom.createMemoryRouter( 116 [ 117 { 118 path: 'path/prefix', 119 children: [ 120 { index: true, element: <BuildDefaultTab /> }, 121 { path: 'overview', element: <></> }, 122 ], 123 }, 124 ], 125 { initialEntries: ['/path/prefix/?param#hash'] }, 126 ); 127 128 render( 129 <StoreProvider value={store}> 130 <reactRouterDom.RouterProvider router={router} /> 131 </StoreProvider>, 132 ); 133 134 expect(useNavigateSpy).toHaveBeenCalledTimes(1); 135 const useNavigateSpyResult = useNavigateSpy.mock.results[0]; 136 expect(useNavigateSpyResult.type).toEqual('return'); 137 // Won't happen. Useful for type inference. 138 if (useNavigateSpyResult.type !== 'return') { 139 throw new Error('unreachable'); 140 } 141 const navigateSpy = useNavigateSpyResult.value; 142 expect(navigateSpy).toHaveBeenCalledTimes(1); 143 expect(navigateSpy.mock.calls[0]).toMatchObject([ 144 // The type definition for `.toMatchObject` is incomplete. Cast to any to 145 // make TSC happy. 146 // eslint-disable-next-line @typescript-eslint/no-explicit-any 147 '/path/prefix/overview?param#hash' as any, 148 { replace: true }, 149 ]); 150 }); 151 152 test('should redirect properly when the saved default tab is valid', async () => { 153 // This can happen when we changed/added/removed tab identifiers. 154 applySnapshot(store, { 155 userConfig: { build: { defaultTab: BuildPageTab.Steps } }, 156 }); 157 158 const router = reactRouterDom.createMemoryRouter( 159 [ 160 { 161 path: 'path/prefix', 162 children: [ 163 { index: true, element: <BuildDefaultTab /> }, 164 { path: BuildPageTab.Overview, element: <></> }, 165 { path: BuildPageTab.Steps, element: <></> }, 166 ], 167 }, 168 ], 169 { initialEntries: ['/path/prefix?param#hash'] }, 170 ); 171 172 render( 173 <StoreProvider value={store}> 174 <reactRouterDom.RouterProvider router={router} /> 175 </StoreProvider>, 176 ); 177 178 expect(useNavigateSpy).toHaveBeenCalledTimes(1); 179 const useNavigateSpyResult = useNavigateSpy.mock.results[0]; 180 expect(useNavigateSpyResult.type).toEqual('return'); 181 // Won't happen. Useful for type inference. 182 if (useNavigateSpyResult.type !== 'return') { 183 throw new Error('unreachable'); 184 } 185 const navigateSpy = useNavigateSpyResult.value; 186 expect(navigateSpy).toHaveBeenCalledTimes(1); 187 expect(navigateSpy.mock.calls[0]).toMatchObject([ 188 // The type definition for `.toMatchObject` is incomplete. Cast to any to 189 // make TSC happy. 190 // eslint-disable-next-line @typescript-eslint/no-explicit-any 191 '/path/prefix/steps?param#hash' as any, 192 { replace: true }, 193 ]); 194 }); 195 196 test('should redirect properly when the saved default tab is invalid', async () => { 197 // This can happen when we changed/added/removed tab identifiers. 198 applySnapshot(store, { 199 userConfig: { build: { defaultTab: 'invalid-tab' } }, 200 }); 201 202 const router = reactRouterDom.createMemoryRouter( 203 [ 204 { 205 path: 'path/prefix', 206 children: [ 207 { index: true, element: <BuildDefaultTab /> }, 208 { path: BuildPageTab.Overview, element: <></> }, 209 { path: BuildPageTab.Steps, element: <></> }, 210 ], 211 }, 212 ], 213 { initialEntries: ['/path/prefix?param#hash'] }, 214 ); 215 216 render( 217 <StoreProvider value={store}> 218 <reactRouterDom.RouterProvider router={router} /> 219 </StoreProvider>, 220 ); 221 222 expect(useNavigateSpy).toHaveBeenCalledTimes(1); 223 const useNavigateSpyResult = useNavigateSpy.mock.results[0]; 224 expect(useNavigateSpyResult.type).toEqual('return'); 225 // Won't happen. Useful for type inference. 226 if (useNavigateSpyResult.type !== 'return') { 227 throw new Error('unreachable'); 228 } 229 const navigateSpy = useNavigateSpyResult.value; 230 expect(navigateSpy).toHaveBeenCalledTimes(1); 231 expect(navigateSpy.mock.calls[0]).toMatchObject([ 232 // The type definition for `.toMatchObject` is incomplete. Cast to any to 233 // make TSC happy. 234 // eslint-disable-next-line @typescript-eslint/no-explicit-any 235 '/path/prefix/overview?param#hash' as any, 236 { replace: true }, 237 ]); 238 }); 239 });