vitess.io/vitess@v0.16.2/web/vtadmin/src/hooks/useURLQuery.test.tsx (about) 1 /** 2 * Copyright 2021 The Vitess Authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 import { act } from '@testing-library/react'; 17 import { renderHook } from '@testing-library/react-hooks'; 18 import { createMemoryHistory } from 'history'; 19 import { Router } from 'react-router-dom'; 20 import { QueryParams } from '../util/queryString'; 21 22 import { useURLQuery, URLQueryOptions } from './useURLQuery'; 23 24 describe('useURLQuery', () => { 25 describe('parsing', () => { 26 const tests: { 27 name: string; 28 url: string; 29 opts: URLQueryOptions | undefined; 30 expected: ReturnType<typeof useURLQuery>['query']; 31 }[] = [ 32 { 33 name: 'parses complex URLs', 34 url: '/test?page=1&isTrue=true&isFalse=false&list=one&list=two&list=three&foo=bar', 35 opts: undefined, 36 expected: { page: '1', isTrue: 'true', isFalse: 'false', list: ['one', 'two', 'three'], foo: 'bar' }, 37 }, 38 { 39 name: 'parses numbers', 40 url: '/test?page=1', 41 opts: { parseNumbers: true }, 42 expected: { page: 1 }, 43 }, 44 { 45 name: 'parses booleans', 46 url: '/test?foo=true&bar=false', 47 opts: { parseBooleans: true }, 48 expected: { foo: true, bar: false }, 49 }, 50 { 51 name: 'parses arrays by duplicate keys', 52 url: '/test?list=1&list=2&list=3', 53 opts: { parseNumbers: true }, 54 expected: { list: [1, 2, 3] }, 55 }, 56 ]; 57 58 test.each(tests.map(Object.values))( 59 '%s', 60 ( 61 name: string, 62 url: string, 63 opts: URLQueryOptions | undefined, 64 expected: ReturnType<typeof useURLQuery> 65 ) => { 66 const history = createMemoryHistory({ 67 initialEntries: [url], 68 }); 69 70 const { result } = renderHook(() => useURLQuery(opts), { 71 wrapper: ({ children }) => { 72 return <Router history={history}>{children}</Router>; 73 }, 74 }); 75 76 expect(result.current.query).toEqual(expected); 77 } 78 ); 79 }); 80 81 describe('pushQuery', () => { 82 const tests: { 83 name: string; 84 initialEntries: string[]; 85 nextQuery: QueryParams; 86 expected: QueryParams; 87 }[] = [ 88 { 89 name: 'stringifies and pushes query parameters onto history', 90 initialEntries: ['/test'], 91 nextQuery: { goodnight: 'moon' }, 92 expected: { goodnight: 'moon' }, 93 }, 94 { 95 name: 'merges the next query with the current query', 96 initialEntries: ['/test?hello=world'], 97 nextQuery: { goodnight: 'moon' }, 98 expected: { hello: 'world', goodnight: 'moon' }, 99 }, 100 { 101 name: 'it does not merge array-like queries', 102 initialEntries: ['/test?arr=one&arr=two'], 103 nextQuery: { arr: ['three', 'four', 'five'] }, 104 expected: { arr: ['three', 'four', 'five'] }, 105 }, 106 ]; 107 108 test.concurrent.each(tests.map(Object.values))( 109 '%s', 110 (name: string, initialEntries: string[], nextQuery: QueryParams, expected: QueryParams) => { 111 const history = createMemoryHistory({ initialEntries }); 112 const initialPathname = history.location.pathname; 113 114 jest.spyOn(history, 'push'); 115 116 const { result } = renderHook(() => useURLQuery(), { 117 wrapper: ({ children }) => { 118 return <Router history={history}>{children}</Router>; 119 }, 120 }); 121 122 act(() => { 123 result.current.pushQuery(nextQuery); 124 }); 125 126 expect(history.push).toHaveBeenCalledTimes(1); 127 expect(result.current.query).toEqual(expected); 128 expect(history.location.pathname).toEqual(initialPathname); 129 } 130 ); 131 }); 132 133 describe('replaceQuery', () => { 134 const tests: { 135 name: string; 136 initialEntries: string[]; 137 nextQuery: QueryParams; 138 expected: QueryParams; 139 }[] = [ 140 { 141 name: 'stringifies and replaces query parameters onto history', 142 initialEntries: ['/test'], 143 nextQuery: { goodnight: 'moon' }, 144 expected: { goodnight: 'moon' }, 145 }, 146 { 147 name: 'merges the next query with the current query', 148 initialEntries: ['/test?hello=world'], 149 nextQuery: { goodnight: 'moon' }, 150 expected: { hello: 'world', goodnight: 'moon' }, 151 }, 152 { 153 name: 'it does not merge array-like queries', 154 initialEntries: ['/test?arr=one&arr=two'], 155 nextQuery: { arr: ['three', 'four', 'five'] }, 156 expected: { arr: ['three', 'four', 'five'] }, 157 }, 158 ]; 159 160 test.concurrent.each(tests.map(Object.values))( 161 '%s', 162 (name: string, initialEntries: string[], nextQuery: QueryParams, expected: QueryParams) => { 163 const history = createMemoryHistory({ initialEntries }); 164 const initialPathname = history.location.pathname; 165 166 jest.spyOn(history, 'replace'); 167 168 const { result } = renderHook(() => useURLQuery(), { 169 wrapper: ({ children }) => { 170 return <Router history={history}>{children}</Router>; 171 }, 172 }); 173 174 act(() => { 175 result.current.replaceQuery(nextQuery); 176 }); 177 178 expect(history.replace).toHaveBeenCalledTimes(1); 179 expect(result.current.query).toEqual(expected); 180 expect(history.location.pathname).toEqual(initialPathname); 181 } 182 ); 183 }); 184 185 it('uses parsing/formatting options when specified', () => { 186 const history = createMemoryHistory({ initialEntries: ['/test?foo=true&count=123'] }); 187 const { result } = renderHook( 188 () => 189 useURLQuery({ 190 parseBooleans: false, 191 parseNumbers: false, 192 }), 193 { 194 wrapper: ({ children }) => { 195 return <Router history={history}>{children}</Router>; 196 }, 197 } 198 ); 199 200 expect(result.current.query).toEqual({ foo: 'true', count: '123' }); 201 202 act(() => { 203 result.current.pushQuery({ foo: false, count: 456 }); 204 }); 205 206 expect(result.current.query).toEqual({ foo: 'false', count: '456' }); 207 208 act(() => { 209 result.current.pushQuery({ foo: true, count: 789 }); 210 }); 211 212 expect(result.current.query).toEqual({ foo: 'true', count: '789' }); 213 }); 214 215 it('memoizes the query object by search string', () => { 216 const history = createMemoryHistory({ initialEntries: ['/test?hello=world'] }); 217 218 jest.spyOn(history, 'push'); 219 220 const { result } = renderHook(() => useURLQuery(), { 221 wrapper: ({ children }) => { 222 return <Router history={history}>{children}</Router>; 223 }, 224 }); 225 226 const firstResult = result.current.query; 227 expect(firstResult).toEqual({ hello: 'world' }); 228 229 act(() => { 230 result.current.pushQuery({ hello: 'world' }); 231 }); 232 233 // Make sure the returned object is memoized when the search string 234 // is updated but the value doesn't change. 235 expect(history.push).toHaveBeenCalledTimes(1); 236 expect(result.current.query).toEqual({ hello: 'world' }); 237 expect(result.current.query).toBe(firstResult); 238 239 // Make sure the returned query actually changes when the search string changes, 240 // and that we're not memoizing too aggressively. 241 act(() => { 242 result.current.pushQuery({ hello: 'moon' }); 243 }); 244 245 expect(history.push).toHaveBeenCalledTimes(2); 246 expect(result.current.query).toEqual({ hello: 'moon' }); 247 }); 248 });