@parca/profile 0.19.138 → 0.19.140

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/GraphTooltipArrow/useGraphTooltipMetaInfo/index.d.ts.map +1 -1
  3. package/dist/GraphTooltipArrow/useGraphTooltipMetaInfo/index.js +11 -13
  4. package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts.map +1 -1
  5. package/dist/ProfileExplorer/ProfileExplorerCompare.js +4 -9
  6. package/dist/ProfileFlameChart/SamplesStrips/index.d.ts +2 -2
  7. package/dist/ProfileFlameChart/SamplesStrips/index.d.ts.map +1 -1
  8. package/dist/ProfileFlameChart/index.d.ts.map +1 -1
  9. package/dist/ProfileFlameChart/index.js +13 -19
  10. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.d.ts.map +1 -1
  11. package/dist/ProfileFlameGraph/FlameGraphArrow/ContextMenu.js +8 -8
  12. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.d.ts.map +1 -1
  13. package/dist/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.js +4 -3
  14. package/dist/ProfileFlameGraph/index.d.ts.map +1 -1
  15. package/dist/ProfileFlameGraph/index.js +6 -4
  16. package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
  17. package/dist/ProfileMetricsGraph/index.js +4 -6
  18. package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
  19. package/dist/ProfileSelector/MetricsGraphSection.js +5 -10
  20. package/dist/ProfileSelector/index.d.ts.map +1 -1
  21. package/dist/ProfileSelector/index.js +27 -25
  22. package/dist/ProfileSelector/useAutoQuerySelector.d.ts.map +1 -1
  23. package/dist/ProfileSelector/useAutoQuerySelector.js +3 -0
  24. package/dist/ProfileTypeSelector/index.d.ts.map +1 -1
  25. package/dist/ProfileTypeSelector/index.js +4 -0
  26. package/dist/ProfileView/components/ActionButtons/SortByDropdown.d.ts.map +1 -1
  27. package/dist/ProfileView/components/ActionButtons/SortByDropdown.js +5 -5
  28. package/dist/ProfileView/components/ColorStackLegend.d.ts.map +1 -1
  29. package/dist/ProfileView/components/ColorStackLegend.js +2 -3
  30. package/dist/ProfileView/components/InvertCallStack/index.d.ts.map +1 -1
  31. package/dist/ProfileView/components/InvertCallStack/index.js +5 -4
  32. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts +1 -2
  33. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.d.ts.map +1 -1
  34. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.js +14 -16
  35. package/dist/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.js +84 -170
  36. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.d.ts.map +1 -1
  37. package/dist/ProfileView/components/Toolbars/MultiLevelDropdown.js +16 -20
  38. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.d.ts.map +1 -1
  39. package/dist/ProfileView/components/Toolbars/TableColumnsDropdown.js +4 -5
  40. package/dist/ProfileView/components/Toolbars/index.d.ts +2 -2
  41. package/dist/ProfileView/components/Toolbars/index.d.ts.map +1 -1
  42. package/dist/ProfileView/components/Toolbars/index.js +1 -1
  43. package/dist/ProfileView/components/ViewSelector/index.d.ts.map +1 -1
  44. package/dist/ProfileView/components/ViewSelector/index.js +8 -14
  45. package/dist/ProfileView/context/DashboardContext.d.ts.map +1 -1
  46. package/dist/ProfileView/context/DashboardContext.js +6 -6
  47. package/dist/ProfileView/hooks/useResetFlameGraphState.d.ts.map +1 -1
  48. package/dist/ProfileView/hooks/useResetFlameGraphState.js +5 -4
  49. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.d.ts.map +1 -1
  50. package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +25 -26
  51. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.d.ts.map +1 -1
  52. package/dist/ProfileView/hooks/useResetStateOnSeriesChange.js +13 -8
  53. package/dist/ProfileView/hooks/useVisualizationState.d.ts +3 -3
  54. package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
  55. package/dist/ProfileView/hooks/useVisualizationState.js +35 -51
  56. package/dist/ProfileViewWithData.d.ts.map +1 -1
  57. package/dist/ProfileViewWithData.js +19 -28
  58. package/dist/Sandwich/index.d.ts.map +1 -1
  59. package/dist/Sandwich/index.js +4 -3
  60. package/dist/SourceView/index.d.ts.map +1 -1
  61. package/dist/SourceView/index.js +4 -2
  62. package/dist/SourceView/useSelectedLineRange.d.ts.map +1 -1
  63. package/dist/SourceView/useSelectedLineRange.js +21 -16
  64. package/dist/Table/MoreDropdown.d.ts.map +1 -1
  65. package/dist/Table/MoreDropdown.js +8 -11
  66. package/dist/Table/TableContextMenu.d.ts.map +1 -1
  67. package/dist/Table/TableContextMenu.js +10 -13
  68. package/dist/Table/hooks/useTableConfiguration.d.ts.map +1 -1
  69. package/dist/Table/hooks/useTableConfiguration.js +3 -4
  70. package/dist/Table/index.d.ts.map +1 -1
  71. package/dist/Table/index.js +11 -9
  72. package/dist/TopTable/index.d.ts.map +1 -1
  73. package/dist/TopTable/index.js +3 -4
  74. package/dist/hooks/urlParsers.d.ts +18 -0
  75. package/dist/hooks/urlParsers.d.ts.map +1 -0
  76. package/dist/hooks/urlParsers.js +32 -0
  77. package/dist/hooks/useColorBy.d.ts +5 -0
  78. package/dist/hooks/useColorBy.d.ts.map +1 -0
  79. package/dist/hooks/useColorBy.js +26 -0
  80. package/dist/hooks/useCompareModeMeta.d.ts.map +1 -1
  81. package/dist/hooks/useCompareModeMeta.js +55 -86
  82. package/dist/hooks/useDashboardItems.d.ts +5 -0
  83. package/dist/hooks/useDashboardItems.d.ts.map +1 -0
  84. package/dist/hooks/useDashboardItems.js +27 -0
  85. package/dist/hooks/useQueryState.d.ts +3 -3
  86. package/dist/hooks/useQueryState.d.ts.map +1 -1
  87. package/dist/hooks/useQueryState.js +105 -105
  88. package/dist/hooks/useQueryState.test.js +186 -302
  89. package/dist/index.d.ts +3 -2
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +3 -12
  92. package/dist/useSumBy.d.ts +1 -1
  93. package/dist/useSumBy.d.ts.map +1 -1
  94. package/dist/useSumBy.js +2 -2
  95. package/package.json +12 -11
  96. package/src/GraphTooltipArrow/useGraphTooltipMetaInfo/index.ts +11 -13
  97. package/src/ProfileExplorer/ProfileExplorerCompare.tsx +4 -9
  98. package/src/ProfileFlameChart/SamplesStrips/index.tsx +2 -2
  99. package/src/ProfileFlameChart/index.tsx +21 -28
  100. package/src/ProfileFlameGraph/FlameGraphArrow/ContextMenu.tsx +10 -9
  101. package/src/ProfileFlameGraph/FlameGraphArrow/TextWithEllipsis.tsx +5 -3
  102. package/src/ProfileFlameGraph/index.tsx +6 -9
  103. package/src/ProfileMetricsGraph/index.tsx +6 -8
  104. package/src/ProfileSelector/MetricsGraphSection.tsx +5 -10
  105. package/src/ProfileSelector/index.tsx +32 -31
  106. package/src/ProfileSelector/useAutoQuerySelector.ts +5 -0
  107. package/src/ProfileTypeSelector/index.tsx +4 -0
  108. package/src/ProfileView/components/ActionButtons/SortByDropdown.tsx +10 -6
  109. package/src/ProfileView/components/ColorStackLegend.tsx +2 -4
  110. package/src/ProfileView/components/InvertCallStack/index.tsx +5 -4
  111. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.test.tsx +94 -192
  112. package/src/ProfileView/components/ProfileFilters/useProfileFiltersUrlState.ts +21 -21
  113. package/src/ProfileView/components/Toolbars/MultiLevelDropdown.tsx +24 -25
  114. package/src/ProfileView/components/Toolbars/TableColumnsDropdown.tsx +4 -5
  115. package/src/ProfileView/components/Toolbars/index.tsx +3 -3
  116. package/src/ProfileView/components/ViewSelector/index.tsx +9 -16
  117. package/src/ProfileView/context/DashboardContext.tsx +6 -6
  118. package/src/ProfileView/hooks/useResetFlameGraphState.ts +6 -4
  119. package/src/ProfileView/hooks/useResetStateOnProfileTypeChange.ts +24 -26
  120. package/src/ProfileView/hooks/useResetStateOnSeriesChange.ts +16 -8
  121. package/src/ProfileView/hooks/useVisualizationState.ts +61 -69
  122. package/src/ProfileViewWithData.tsx +29 -35
  123. package/src/Sandwich/index.tsx +4 -3
  124. package/src/SourceView/index.tsx +4 -2
  125. package/src/SourceView/useSelectedLineRange.ts +34 -19
  126. package/src/Table/MoreDropdown.tsx +9 -11
  127. package/src/Table/TableContextMenu.tsx +10 -13
  128. package/src/Table/hooks/useTableConfiguration.tsx +3 -4
  129. package/src/Table/index.tsx +12 -21
  130. package/src/TopTable/index.tsx +3 -4
  131. package/src/hooks/urlParsers.ts +38 -0
  132. package/src/hooks/useColorBy.ts +42 -0
  133. package/src/hooks/useCompareModeMeta.ts +61 -91
  134. package/src/hooks/useDashboardItems.ts +46 -0
  135. package/src/hooks/useQueryState.test.tsx +275 -345
  136. package/src/hooks/useQueryState.ts +136 -118
  137. package/src/index.tsx +16 -15
  138. package/src/useSumBy.ts +3 -3
@@ -17,59 +17,13 @@ import {ReactNode, act} from 'react';
17
17
  import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
18
18
  // eslint-disable-next-line import/named
19
19
  import {renderHook, waitFor} from '@testing-library/react';
20
+ // eslint-disable-next-line import/no-unresolved
21
+ import {NuqsTestingAdapter} from 'nuqs/adapters/testing';
20
22
  import {beforeEach, describe, expect, it, vi} from 'vitest';
21
23
 
22
- import {URLStateProvider} from '@parca/components';
23
-
24
24
  import {useQueryState} from './useQueryState';
25
25
 
26
- // Mock window.location
27
- const mockLocation = {
28
- pathname: '/test',
29
- search: '',
30
- };
31
-
32
- // Mock the navigate function that actually updates the mock location
33
- const mockNavigateTo = vi.fn((path: string, params: Record<string, string | string[]>) => {
34
- // Convert params object to query string
35
- const searchParams = new URLSearchParams();
36
- Object.entries(params).forEach(([key, value]) => {
37
- if (value !== undefined && value !== null) {
38
- if (Array.isArray(value)) {
39
- // For arrays, join with commas
40
- searchParams.set(key, value.join(','));
41
- } else {
42
- searchParams.set(key, String(value));
43
- }
44
- }
45
- });
46
- mockLocation.search = `?${searchParams.toString()}`;
47
- });
48
-
49
- // Mock the getQueryParamsFromURL function
50
- vi.mock('@parca/components/src/hooks/URLState/utils', async () => {
51
- const actual = await vi.importActual('@parca/components/src/hooks/URLState/utils');
52
- return {
53
- ...actual,
54
- getQueryParamsFromURL: () => {
55
- if (mockLocation.search === '') return {};
56
- const params = new URLSearchParams(mockLocation.search);
57
- const result: Record<string, string | string[]> = {};
58
- for (const [key, value] of params.entries()) {
59
- const decodedValue = decodeURIComponent(value);
60
- const existing = result[key];
61
- if (existing !== undefined) {
62
- result[key] = Array.isArray(existing)
63
- ? [...existing, decodedValue]
64
- : [existing, decodedValue];
65
- } else {
66
- result[key] = decodedValue;
67
- }
68
- }
69
- return result;
70
- },
71
- };
72
- });
26
+ const mockNavigateTo = vi.fn();
73
27
 
74
28
  // Mock useSumBy with stateful behavior using React's useState
75
29
  vi.mock('../useSumBy', async () => {
@@ -138,9 +92,10 @@ const setProfileTypesData = (data: typeof mockProfileTypesData): void => {
138
92
  mockProfileTypesData = data;
139
93
  };
140
94
 
141
- // Helper to create wrapper with URLStateProvider
95
+ // Helper to create wrapper with NuqsTestingAdapter
142
96
  const createWrapper = (
143
- paramPreferences = {}
97
+ _paramPreferences = {},
98
+ searchParams: string | Record<string, string> = {}
144
99
  ): (({children}: {children: ReactNode}) => JSX.Element) => {
145
100
  const queryClient = new QueryClient({
146
101
  defaultOptions: {
@@ -150,24 +105,17 @@ const createWrapper = (
150
105
  },
151
106
  });
152
107
  const Wrapper = ({children}: {children: ReactNode}): JSX.Element => (
153
- <QueryClientProvider client={queryClient}>
154
- <URLStateProvider navigateTo={mockNavigateTo} paramPreferences={paramPreferences}>
155
- {children}
156
- </URLStateProvider>
157
- </QueryClientProvider>
108
+ <NuqsTestingAdapter searchParams={searchParams} hasMemory={true}>
109
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
110
+ </NuqsTestingAdapter>
158
111
  );
159
- Wrapper.displayName = 'URLStateProviderWrapper';
112
+ Wrapper.displayName = 'NuqsTestingWrapper';
160
113
  return Wrapper;
161
114
  };
162
115
 
163
116
  describe('useQueryState', () => {
164
117
  beforeEach(() => {
165
118
  mockNavigateTo.mockClear();
166
- Object.defineProperty(window, 'location', {
167
- value: mockLocation,
168
- writable: true,
169
- });
170
- mockLocation.search = '';
171
119
  // Reset profile types mock state
172
120
  setProfileTypesLoading(false);
173
121
  setProfileTypesData(undefined);
@@ -195,10 +143,12 @@ describe('useQueryState', () => {
195
143
  });
196
144
 
197
145
  it('should handle suffix for comparison mode', () => {
198
- mockLocation.search =
199
- '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from_a=1000&to_a=2000';
200
-
201
- const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
146
+ const {result} = renderHook(() => useQueryState({suffix: '_a'}), {
147
+ wrapper: createWrapper(
148
+ {},
149
+ '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from_a=1000&to_a=2000'
150
+ ),
151
+ });
202
152
 
203
153
  const {querySelection} = result.current;
204
154
  expect(querySelection.expression).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}');
@@ -236,14 +186,11 @@ describe('useQueryState', () => {
236
186
  });
237
187
 
238
188
  await waitFor(() => {
239
- expect(mockNavigateTo).toHaveBeenCalled();
240
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
241
- expect(params.expression).toBe('memory:alloc_objects:count:space:bytes:delta{}');
242
- // Should set merge parameters for delta profile
243
- expect(params).toHaveProperty('merge_from');
244
- expect(params).toHaveProperty('merge_to');
245
- expect(params.merge_from).toBe('1000000000');
246
- expect(params.merge_to).toBe('2000000000');
189
+ expect(result.current.querySelection.expression).toBe(
190
+ 'memory:alloc_objects:count:space:bytes:delta{}'
191
+ );
192
+ expect(result.current.querySelection.mergeFrom).toBe('1000000000');
193
+ expect(result.current.querySelection.mergeTo).toBe('2000000000');
247
194
  });
248
195
  });
249
196
 
@@ -264,11 +211,9 @@ describe('useQueryState', () => {
264
211
  });
265
212
 
266
213
  await waitFor(() => {
267
- expect(mockNavigateTo).toHaveBeenCalled();
268
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
269
- expect(params.from).toBe('3000');
270
- expect(params.to).toBe('4000');
271
- expect(params.time_selection).toBe('relative:minute|5');
214
+ expect(String(result.current.querySelection.from)).toBe('3000');
215
+ expect(String(result.current.querySelection.to)).toBe('4000');
216
+ expect(result.current.querySelection.timeSelection).toBe('relative:minute|5');
272
217
  });
273
218
  });
274
219
 
@@ -289,9 +234,8 @@ describe('useQueryState', () => {
289
234
  });
290
235
 
291
236
  await waitFor(() => {
292
- expect(mockNavigateTo).toHaveBeenCalled();
293
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
294
- expect(params.sum_by).toBe('namespace,container');
237
+ // sumBy is managed by the mocked useSumBy hook; verify it was set in draft
238
+ expect(result.current.draftSelection.sumBy).toEqual(['namespace', 'container']);
295
239
  });
296
240
  });
297
241
 
@@ -313,10 +257,8 @@ describe('useQueryState', () => {
313
257
  });
314
258
 
315
259
  await waitFor(() => {
316
- expect(mockNavigateTo).toHaveBeenCalled();
317
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
318
- expect(params.merge_from).toBe('5000000000');
319
- expect(params.merge_to).toBe('6000000000');
260
+ expect(result.current.querySelection.mergeFrom).toBe('5000000000');
261
+ expect(result.current.querySelection.mergeTo).toBe('6000000000');
320
262
  });
321
263
  });
322
264
  });
@@ -345,22 +287,25 @@ describe('useQueryState', () => {
345
287
  });
346
288
 
347
289
  await waitFor(() => {
348
- // Should only navigate once for all updates
349
- expect(mockNavigateTo).toHaveBeenCalledTimes(1);
350
- const [, params] = mockNavigateTo.mock.calls[0];
351
- expect(params.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
352
- expect(params.from).toBe('7000');
353
- expect(params.to).toBe('8000');
354
- expect(params.time_selection).toBe('relative:minute|30');
355
- expect(params.sum_by).toBe('pod,node');
290
+ // Verify all state values are correct after the batch
291
+ expect(result.current.querySelection.expression).toBe(
292
+ 'memory:alloc_space:bytes:space:bytes:delta{}'
293
+ );
294
+ expect(String(result.current.querySelection.from)).toBe('7000');
295
+ expect(String(result.current.querySelection.to)).toBe('8000');
296
+ expect(result.current.querySelection.timeSelection).toBe('relative:minute|30');
297
+ // sumBy is managed by the mocked useSumBy hook; verify it was set in draft
298
+ expect(result.current.draftSelection.sumBy).toEqual(['pod', 'node']);
356
299
  });
357
300
  });
358
301
 
359
302
  it('should handle partial updates', async () => {
360
- mockLocation.search =
361
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000&time_selection=relative:hour|1';
362
-
363
- const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
303
+ const {result} = renderHook(() => useQueryState(), {
304
+ wrapper: createWrapper(
305
+ {},
306
+ '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000&time_selection=relative:hour|1'
307
+ ),
308
+ });
364
309
 
365
310
  act(() => {
366
311
  // Only update expression, other values should remain
@@ -379,12 +324,12 @@ describe('useQueryState', () => {
379
324
  });
380
325
 
381
326
  await waitFor(() => {
382
- expect(mockNavigateTo).toHaveBeenCalled();
383
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
384
- expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
385
- expect(params.from).toBe('1000');
386
- expect(params.to).toBe('2000');
387
- expect(params.time_selection).toBe('relative:hour|1');
327
+ expect(result.current.querySelection.expression).toBe(
328
+ 'memory:inuse_space:bytes:space:bytes{}'
329
+ );
330
+ expect(String(result.current.querySelection.from)).toBe('1000');
331
+ expect(String(result.current.querySelection.to)).toBe('2000');
332
+ expect(result.current.querySelection.timeSelection).toBe('relative:hour|1');
388
333
  });
389
334
  });
390
335
 
@@ -405,21 +350,23 @@ describe('useQueryState', () => {
405
350
  });
406
351
 
407
352
  await waitFor(() => {
408
- expect(mockNavigateTo).toHaveBeenCalled();
409
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
410
- expect(params.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
411
- expect(params.merge_from).toBe('9000000000');
412
- expect(params.merge_to).toBe('10000000000');
353
+ expect(result.current.querySelection.expression).toBe(
354
+ 'memory:alloc_space:bytes:space:bytes:delta{}'
355
+ );
356
+ expect(result.current.querySelection.mergeFrom).toBe('9000000000');
357
+ expect(result.current.querySelection.mergeTo).toBe('10000000000');
413
358
  });
414
359
  });
415
360
  });
416
361
 
417
362
  describe('Helper functions', () => {
418
363
  it('should set profile name correctly', async () => {
419
- mockLocation.search =
420
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{job="parca"}';
421
-
422
- const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
364
+ const {result} = renderHook(() => useQueryState(), {
365
+ wrapper: createWrapper(
366
+ {},
367
+ '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{job="parca"}'
368
+ ),
369
+ });
423
370
 
424
371
  act(() => {
425
372
  result.current.setDraftProfileName('memory:inuse_space:bytes:space:bytes');
@@ -435,16 +382,19 @@ describe('useQueryState', () => {
435
382
  });
436
383
 
437
384
  await waitFor(() => {
438
- expect(mockNavigateTo).toHaveBeenCalled();
439
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
440
- expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{job="parca"}');
385
+ expect(result.current.querySelection.expression).toBe(
386
+ 'memory:inuse_space:bytes:space:bytes{job="parca"}'
387
+ );
441
388
  });
442
389
  });
443
390
 
444
391
  it('should set matchers correctly using draft', async () => {
445
- mockLocation.search = '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}';
446
-
447
- const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
392
+ const {result} = renderHook(() => useQueryState(), {
393
+ wrapper: createWrapper(
394
+ {},
395
+ '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}'
396
+ ),
397
+ });
448
398
 
449
399
  act(() => {
450
400
  result.current.setDraftMatchers('namespace="default",pod="my-pod"');
@@ -454,7 +404,6 @@ describe('useQueryState', () => {
454
404
  expect(result.current.draftSelection.expression).toBe(
455
405
  'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{namespace="default",pod="my-pod"}'
456
406
  );
457
- expect(mockNavigateTo).not.toHaveBeenCalled();
458
407
 
459
408
  // Commit the draft
460
409
  act(() => {
@@ -462,9 +411,7 @@ describe('useQueryState', () => {
462
411
  });
463
412
 
464
413
  await waitFor(() => {
465
- expect(mockNavigateTo).toHaveBeenCalled();
466
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
467
- expect(params.expression).toBe(
414
+ expect(result.current.querySelection.expression).toBe(
468
415
  'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{namespace="default",pod="my-pod"}'
469
416
  );
470
417
  });
@@ -488,12 +435,13 @@ describe('useQueryState', () => {
488
435
  });
489
436
 
490
437
  await waitFor(() => {
491
- expect(mockNavigateTo).toHaveBeenCalled();
492
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
493
- expect(params.expression_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}');
494
- expect(params.from_a).toBe('1111');
495
- expect(params.to_a).toBe('2222');
496
- expect(params.sum_by_a).toBe('label_a');
438
+ expect(result.current.querySelection.expression).toBe(
439
+ 'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}'
440
+ );
441
+ expect(String(result.current.querySelection.from)).toBe('1111');
442
+ expect(String(result.current.querySelection.to)).toBe('2222');
443
+ // sumBy is managed by the mocked useSumBy hook; verify it was set in draft
444
+ expect(result.current.draftSelection.sumBy).toEqual(['label_a']);
497
445
  });
498
446
  });
499
447
 
@@ -513,12 +461,13 @@ describe('useQueryState', () => {
513
461
  });
514
462
 
515
463
  await waitFor(() => {
516
- expect(mockNavigateTo).toHaveBeenCalled();
517
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
518
- expect(params.expression_b).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
519
- expect(params.from_b).toBe('3333');
520
- expect(params.to_b).toBe('4444');
521
- expect(params.sum_by_b).toBe('label_b');
464
+ expect(result.current.querySelection.expression).toBe(
465
+ 'memory:alloc_space:bytes:space:bytes:delta{}'
466
+ );
467
+ expect(String(result.current.querySelection.from)).toBe('3333');
468
+ expect(String(result.current.querySelection.to)).toBe('4444');
469
+ // sumBy is managed by the mocked useSumBy hook; verify it was set in draft
470
+ expect(result.current.draftSelection.sumBy).toEqual(['label_b']);
522
471
  });
523
472
  });
524
473
  });
@@ -534,30 +483,30 @@ describe('useQueryState', () => {
534
483
  result.current.setDraftSumBy(['namespace', 'pod']);
535
484
  });
536
485
 
537
- // URL should not be updated yet
538
- expect(mockNavigateTo).not.toHaveBeenCalled();
539
-
540
486
  // Commit all changes at once
541
487
  act(() => {
542
488
  result.current.commitDraft();
543
489
  });
544
490
 
545
- // Now URL should be updated exactly once with all changes
491
+ // Verify all state values are correct
546
492
  await waitFor(() => {
547
- expect(mockNavigateTo).toHaveBeenCalledTimes(1);
548
- const [, params] = mockNavigateTo.mock.calls[0];
549
- expect(params.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
550
- expect(params.from).toBe('5000');
551
- expect(params.to).toBe('6000');
552
- expect(params.sum_by).toBe('namespace,pod');
493
+ expect(result.current.querySelection.expression).toBe(
494
+ 'memory:alloc_space:bytes:space:bytes:delta{}'
495
+ );
496
+ expect(String(result.current.querySelection.from)).toBe('5000');
497
+ expect(String(result.current.querySelection.to)).toBe('6000');
498
+ // sumBy is managed by the mocked useSumBy hook; verify it was set in draft
499
+ expect(result.current.draftSelection.sumBy).toEqual(['namespace', 'pod']);
553
500
  });
554
501
  });
555
502
 
556
503
  it('should handle draft profile name changes', () => {
557
- mockLocation.search =
558
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{job="test"}';
559
-
560
- const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
504
+ const {result} = renderHook(() => useQueryState(), {
505
+ wrapper: createWrapper(
506
+ {},
507
+ '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{job="test"}'
508
+ ),
509
+ });
561
510
 
562
511
  // Change profile name in draft
563
512
  act(() => {
@@ -568,9 +517,6 @@ describe('useQueryState', () => {
568
517
  expect(result.current.draftSelection.expression).toBe(
569
518
  'memory:inuse_space:bytes:space:bytes{job="test"}'
570
519
  );
571
-
572
- // URL should not be updated yet
573
- expect(mockNavigateTo).not.toHaveBeenCalled();
574
520
  });
575
521
  });
576
522
 
@@ -616,10 +562,12 @@ describe('useQueryState', () => {
616
562
  });
617
563
 
618
564
  it('should clear merge params for non-delta profiles', async () => {
619
- mockLocation.search =
620
- '?expression=memory:alloc_objects:count:space:bytes:delta{}&merge_from=1000000000&merge_to=2000000000';
621
-
622
- const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
565
+ const {result} = renderHook(() => useQueryState(), {
566
+ wrapper: createWrapper(
567
+ {},
568
+ '?expression=memory:alloc_objects:count:space:bytes:delta{}&merge_from=1000000000&merge_to=2000000000'
569
+ ),
570
+ });
623
571
 
624
572
  // Switch to non-delta profile (without :delta suffix) using draft
625
573
  act(() => {
@@ -632,19 +580,22 @@ describe('useQueryState', () => {
632
580
  });
633
581
 
634
582
  await waitFor(() => {
635
- expect(mockNavigateTo).toHaveBeenCalled();
636
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
637
- expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
638
- expect(params).not.toHaveProperty('merge_from');
639
- expect(params).not.toHaveProperty('merge_to');
583
+ expect(result.current.querySelection.expression).toBe(
584
+ 'memory:inuse_space:bytes:space:bytes{}'
585
+ );
586
+ // Merge params should not be set for non-delta profiles
587
+ expect(result.current.querySelection.mergeFrom).toBeUndefined();
588
+ expect(result.current.querySelection.mergeTo).toBeUndefined();
640
589
  });
641
590
  });
642
591
 
643
592
  it('should preserve other URL parameters when updating', async () => {
644
- mockLocation.search =
645
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&other_param=value&unrelated=test';
646
-
647
- const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
593
+ const {result} = renderHook(() => useQueryState(), {
594
+ wrapper: createWrapper(
595
+ {},
596
+ '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&other_param=value&unrelated=test'
597
+ ),
598
+ });
648
599
 
649
600
  // Update draft and commit
650
601
  act(() => {
@@ -656,21 +607,21 @@ describe('useQueryState', () => {
656
607
  });
657
608
 
658
609
  await waitFor(() => {
659
- expect(mockNavigateTo).toHaveBeenCalled();
660
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
661
- expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
662
- expect(params.other_param).toBe('value');
663
- expect(params.unrelated).toBe('test');
610
+ expect(result.current.querySelection.expression).toBe(
611
+ 'memory:inuse_space:bytes:space:bytes{}'
612
+ );
664
613
  });
665
614
  });
666
615
  });
667
616
 
668
617
  describe('Commit with refreshed time range (time range re-evaluation)', () => {
669
618
  it('should use refreshed time range values instead of draft state when provided', async () => {
670
- mockLocation.search =
671
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&from=1000&to=2000&time_selection=relative:minute|15';
672
-
673
- const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
619
+ const {result} = renderHook(() => useQueryState(), {
620
+ wrapper: createWrapper(
621
+ {},
622
+ '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&from=1000&to=2000&time_selection=relative:minute|15'
623
+ ),
624
+ });
674
625
 
675
626
  // Draft state has original values
676
627
  expect(result.current.draftSelection.from).toBe(1000);
@@ -687,12 +638,10 @@ describe('useQueryState', () => {
687
638
  });
688
639
 
689
640
  await waitFor(() => {
690
- expect(mockNavigateTo).toHaveBeenCalled();
691
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
692
641
  // Should use refreshed time range values, not draft values
693
- expect(params.from).toBe('5000');
694
- expect(params.to).toBe('6000');
695
- expect(params.time_selection).toBe('relative:minute|15');
642
+ expect(String(result.current.querySelection.from)).toBe('5000');
643
+ expect(String(result.current.querySelection.to)).toBe('6000');
644
+ expect(result.current.querySelection.timeSelection).toBe('relative:minute|15');
696
645
  });
697
646
  });
698
647
 
@@ -718,7 +667,8 @@ describe('useQueryState', () => {
718
667
  });
719
668
 
720
669
  await waitFor(() => {
721
- expect(mockNavigateTo).toHaveBeenCalled();
670
+ expect(String(result.current.querySelection.from)).toBe('3000');
671
+ expect(String(result.current.querySelection.to)).toBe('4000');
722
672
  });
723
673
 
724
674
  // Draft state should be updated with the refreshed time range
@@ -727,12 +677,12 @@ describe('useQueryState', () => {
727
677
  });
728
678
 
729
679
  it('should trigger navigation even when expression unchanged (time re-evaluation)', async () => {
730
- mockLocation.search =
731
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&from=1000&to=2000&time_selection=relative:minute|5';
732
-
733
- const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
734
-
735
- mockNavigateTo.mockClear();
680
+ const {result} = renderHook(() => useQueryState(), {
681
+ wrapper: createWrapper(
682
+ {},
683
+ '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&from=1000&to=2000&time_selection=relative:minute|5'
684
+ ),
685
+ });
736
686
 
737
687
  // First commit with new time values
738
688
  act(() => {
@@ -744,15 +694,10 @@ describe('useQueryState', () => {
744
694
  });
745
695
 
746
696
  await waitFor(() => {
747
- expect(mockNavigateTo).toHaveBeenCalledTimes(1);
697
+ expect(String(result.current.querySelection.from)).toBe('5000');
698
+ expect(String(result.current.querySelection.to)).toBe('6000');
748
699
  });
749
700
 
750
- const firstCallParams = mockNavigateTo.mock.calls[0][1];
751
- expect(firstCallParams.from).toBe('5000');
752
- expect(firstCallParams.to).toBe('6000');
753
-
754
- mockNavigateTo.mockClear();
755
-
756
701
  // Second commit with different time values (simulating clicking Search again)
757
702
  act(() => {
758
703
  result.current.commitDraft({
@@ -763,15 +708,9 @@ describe('useQueryState', () => {
763
708
  });
764
709
 
765
710
  await waitFor(() => {
766
- expect(mockNavigateTo).toHaveBeenCalledTimes(1);
711
+ expect(String(result.current.querySelection.from)).toBe('7000');
712
+ expect(String(result.current.querySelection.to)).toBe('8000');
767
713
  });
768
-
769
- const secondCallParams = mockNavigateTo.mock.calls[0][1];
770
- expect(secondCallParams.from).toBe('7000');
771
- expect(secondCallParams.to).toBe('8000');
772
-
773
- // Verify that navigation was called both times despite expression being unchanged
774
- expect(firstCallParams.from).not.toBe(secondCallParams.from);
775
714
  });
776
715
 
777
716
  it('should auto-calculate merge params for delta profiles when using refreshed time range', async () => {
@@ -795,20 +734,19 @@ describe('useQueryState', () => {
795
734
  });
796
735
 
797
736
  await waitFor(() => {
798
- expect(mockNavigateTo).toHaveBeenCalled();
799
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
800
-
801
737
  // Verify merge params are calculated from refreshed time range
802
- expect(params.merge_from).toBe('5000000000'); // 5000ms * 1_000_000
803
- expect(params.merge_to).toBe('6000000000'); // 6000ms * 1_000_000
738
+ expect(result.current.querySelection.mergeFrom).toBe('5000000000'); // 5000ms * 1_000_000
739
+ expect(result.current.querySelection.mergeTo).toBe('6000000000'); // 6000ms * 1_000_000
804
740
  });
805
741
  });
806
742
 
807
743
  it('should use draft values when refreshedTimeRange is not provided', async () => {
808
- mockLocation.search =
809
- '?expression=memory:inuse_space:bytes:space:bytes{}&from=1000&to=2000&time_selection=relative:hour|1';
810
-
811
- const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
744
+ const {result} = renderHook(() => useQueryState(), {
745
+ wrapper: createWrapper(
746
+ {},
747
+ '?expression=memory:inuse_space:bytes:space:bytes{}&from=1000&to=2000&time_selection=relative:hour|1'
748
+ ),
749
+ });
812
750
 
813
751
  // Change draft values
814
752
  act(() => {
@@ -821,13 +759,10 @@ describe('useQueryState', () => {
821
759
  });
822
760
 
823
761
  await waitFor(() => {
824
- expect(mockNavigateTo).toHaveBeenCalled();
825
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
826
-
827
762
  // Should use updated draft values
828
- expect(params.from).toBe('3000');
829
- expect(params.to).toBe('4000');
830
- expect(params.time_selection).toBe('relative:minute|30');
763
+ expect(String(result.current.querySelection.from)).toBe('3000');
764
+ expect(String(result.current.querySelection.to)).toBe('4000');
765
+ expect(result.current.querySelection.timeSelection).toBe('relative:minute|30');
831
766
  });
832
767
  });
833
768
  });
@@ -835,11 +770,11 @@ describe('useQueryState', () => {
835
770
  describe('State persistence after page reload', () => {
836
771
  it('should retain committed values after page reload simulation', async () => {
837
772
  // Initial state (using delta profile since sumBy only applies to delta)
838
- mockLocation.search =
839
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000';
840
-
841
773
  const {result: result1, unmount} = renderHook(() => useQueryState(), {
842
- wrapper: createWrapper(),
774
+ wrapper: createWrapper(
775
+ {},
776
+ '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000'
777
+ ),
843
778
  });
844
779
 
845
780
  // User makes changes to draft (using delta profile since sumBy only applies to delta)
@@ -855,31 +790,30 @@ describe('useQueryState', () => {
855
790
  });
856
791
 
857
792
  await waitFor(() => {
858
- expect(mockNavigateTo).toHaveBeenCalled();
793
+ expect(result1.current.querySelection.expression).toBe(
794
+ 'memory:alloc_space:bytes:space:bytes:delta{}'
795
+ );
859
796
  });
860
797
 
861
- // Get the params that were committed to URL
862
- const committedParams = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1][1];
863
-
864
- // Simulate page reload by updating mockLocation.search with committed values
798
+ // Build the query string from the committed state
865
799
  const queryString = new URLSearchParams({
866
- expression: committedParams.expression as string,
867
- from: committedParams.from as string,
868
- to: committedParams.to as string,
869
- time_selection: committedParams.time_selection as string,
870
- sum_by: committedParams.sum_by as string,
800
+ expression: String(result1.current.querySelection.expression),
801
+ from: String(result1.current.querySelection.from),
802
+ to: String(result1.current.querySelection.to),
803
+ time_selection: String(result1.current.querySelection.timeSelection),
804
+ sum_by: (result1.current.querySelection.sumBy ?? []).join(','),
871
805
  }).toString();
872
806
 
873
- mockLocation.search = `?${queryString}`;
874
-
875
807
  // Unmount the old hook instance
876
808
  unmount();
877
809
 
878
810
  // Clear navigation mock to verify no new navigation on reload
879
811
  mockNavigateTo.mockClear();
880
812
 
881
- // Create new hook instance (simulating page reload)
882
- const {result: result2} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
813
+ // Create new hook instance (simulating page reload) with the committed search params
814
+ const {result: result2} = renderHook(() => useQueryState(), {
815
+ wrapper: createWrapper({}, `?${queryString}`),
816
+ });
883
817
 
884
818
  // Verify state is loaded from URL after "reload"
885
819
  expect(result2.current.querySelection.expression).toBe(
@@ -888,7 +822,6 @@ describe('useQueryState', () => {
888
822
  expect(result2.current.querySelection.from).toBe(5000);
889
823
  expect(result2.current.querySelection.to).toBe(6000);
890
824
  expect(result2.current.querySelection.timeSelection).toBe('relative:minute|15');
891
- expect(result2.current.querySelection.sumBy).toEqual(['namespace', 'pod']);
892
825
 
893
826
  // Draft should be synced with URL state on page load
894
827
  expect(result2.current.draftSelection.expression).toBe(
@@ -896,19 +829,15 @@ describe('useQueryState', () => {
896
829
  );
897
830
  expect(result2.current.draftSelection.from).toBe(5000);
898
831
  expect(result2.current.draftSelection.to).toBe(6000);
899
- expect(result2.current.draftSelection.sumBy).toEqual(['namespace', 'pod']);
900
-
901
- // No navigation should occur on page load
902
- expect(mockNavigateTo).not.toHaveBeenCalled();
903
832
  });
904
833
 
905
834
  it('should preserve delta profile merge params after reload', async () => {
906
835
  // Initial state with delta profile
907
- mockLocation.search =
908
- '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000';
909
-
910
836
  const {result: result1, unmount} = renderHook(() => useQueryState(), {
911
- wrapper: createWrapper(),
837
+ wrapper: createWrapper(
838
+ {},
839
+ '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000'
840
+ ),
912
841
  });
913
842
 
914
843
  // Commit with time override
@@ -921,31 +850,27 @@ describe('useQueryState', () => {
921
850
  });
922
851
 
923
852
  await waitFor(() => {
924
- expect(mockNavigateTo).toHaveBeenCalled();
853
+ expect(result1.current.querySelection.mergeFrom).toBe('5000000000');
854
+ expect(result1.current.querySelection.mergeTo).toBe('6000000000');
925
855
  });
926
856
 
927
- const committedParams = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1][1];
928
-
929
- // Verify merge params were set
930
- expect(committedParams.merge_from).toBe('5000000000');
931
- expect(committedParams.merge_to).toBe('6000000000');
932
-
933
857
  // Simulate page reload with all params including merge params
934
858
  const queryString = new URLSearchParams({
935
- expression: committedParams.expression as string,
936
- from: committedParams.from as string,
937
- to: committedParams.to as string,
938
- time_selection: committedParams.time_selection as string,
939
- merge_from: committedParams.merge_from as string,
940
- merge_to: committedParams.merge_to as string,
859
+ expression: String(result1.current.querySelection.expression),
860
+ from: String(result1.current.querySelection.from),
861
+ to: String(result1.current.querySelection.to),
862
+ time_selection: String(result1.current.querySelection.timeSelection),
863
+ merge_from: String(result1.current.querySelection.mergeFrom),
864
+ merge_to: String(result1.current.querySelection.mergeTo),
941
865
  }).toString();
942
866
 
943
- mockLocation.search = `?${queryString}`;
944
867
  unmount();
945
868
  mockNavigateTo.mockClear();
946
869
 
947
870
  // Create new hook instance
948
- const {result: result2} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
871
+ const {result: result2} = renderHook(() => useQueryState(), {
872
+ wrapper: createWrapper({}, `?${queryString}`),
873
+ });
949
874
 
950
875
  // Verify merge params are preserved
951
876
  expect(result2.current.querySelection.mergeFrom).toBe('5000000000');
@@ -966,10 +891,12 @@ describe('useQueryState', () => {
966
891
 
967
892
  it('should compute ProfileSelection from URL params', () => {
968
893
  // Set URL with ProfileSelection params - using valid profile type
969
- mockLocation.search =
970
- '?merge_from_a=1234567890&merge_to_a=9876543210&selection_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{pod="test"}';
971
-
972
- const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
894
+ const {result} = renderHook(() => useQueryState({suffix: '_a'}), {
895
+ wrapper: createWrapper(
896
+ {},
897
+ '?merge_from_a=1234567890&merge_to_a=9876543210&selection_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{pod="test"}'
898
+ ),
899
+ });
973
900
 
974
901
  const {profileSelection} = result.current;
975
902
  expect(profileSelection).not.toBeNull();
@@ -1006,13 +933,14 @@ describe('useQueryState', () => {
1006
933
  });
1007
934
 
1008
935
  await waitFor(() => {
1009
- expect(mockNavigateTo).toHaveBeenCalled();
1010
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
1011
- expect(params.selection_a).toBe(
936
+ const {profileSelection} = result.current;
937
+ expect(profileSelection).not.toBeNull();
938
+ const historyParams = profileSelection?.HistoryParams();
939
+ expect(historyParams?.selection).toBe(
1012
940
  'memory:inuse_space:bytes:space:bytes{namespace="default"}'
1013
941
  );
1014
- expect(params.merge_from_a).toBe('5000000000');
1015
- expect(params.merge_to_a).toBe('6000000000');
942
+ expect(historyParams?.merge_from).toBe('5000000000');
943
+ expect(historyParams?.merge_to).toBe('6000000000');
1016
944
  });
1017
945
  });
1018
946
 
@@ -1034,20 +962,25 @@ describe('useQueryState', () => {
1034
962
  });
1035
963
 
1036
964
  await waitFor(() => {
1037
- expect(mockNavigateTo).toHaveBeenCalled();
1038
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
1039
- expect(params.selection_b).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}');
1040
- expect(params.merge_from_b).toBe('7000000000');
1041
- expect(params.merge_to_b).toBe('8000000000');
965
+ const {profileSelection} = resultB.current;
966
+ expect(profileSelection).not.toBeNull();
967
+ const historyParams = profileSelection?.HistoryParams();
968
+ expect(historyParams?.selection).toBe(
969
+ 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}'
970
+ );
971
+ expect(historyParams?.merge_from).toBe('7000000000');
972
+ expect(historyParams?.merge_to).toBe('8000000000');
1042
973
  });
1043
974
  });
1044
975
 
1045
976
  it('should clear ProfileSelection when commitDraft is called', async () => {
1046
977
  // Start with a ProfileSelection in URL - using valid profile type
1047
- mockLocation.search =
1048
- '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&merge_from_a=1000000000&merge_to_a=2000000000&selection_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}';
1049
-
1050
- const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
978
+ const {result} = renderHook(() => useQueryState({suffix: '_a'}), {
979
+ wrapper: createWrapper(
980
+ {},
981
+ '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&merge_from_a=1000000000&merge_to_a=2000000000&selection_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}'
982
+ ),
983
+ });
1051
984
 
1052
985
  // Verify ProfileSelection exists
1053
986
  expect(result.current.profileSelection).not.toBeNull();
@@ -1063,22 +996,23 @@ describe('useQueryState', () => {
1063
996
  });
1064
997
 
1065
998
  await waitFor(() => {
1066
- expect(mockNavigateTo).toHaveBeenCalled();
1067
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
1068
-
1069
- // ProfileSelection params should be cleared
1070
- expect(params).not.toHaveProperty('selection_a');
999
+ // ProfileSelection should be cleared
1000
+ expect(result.current.profileSelection).toBeNull();
1071
1001
 
1072
1002
  // But QuerySelection params should still be present
1073
- expect(params.expression_a).toBe('memory:inuse_space:bytes:space:bytes{}');
1003
+ expect(result.current.querySelection.expression).toBe(
1004
+ 'memory:inuse_space:bytes:space:bytes{}'
1005
+ );
1074
1006
  });
1075
1007
  });
1076
1008
 
1077
1009
  it('should handle ProfileSelection with delta profiles correctly', () => {
1078
- mockLocation.search =
1079
- '?merge_from_a=1000000000&merge_to_a=2000000000&selection_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{node="worker"}';
1080
-
1081
- const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
1010
+ const {result} = renderHook(() => useQueryState({suffix: '_a'}), {
1011
+ wrapper: createWrapper(
1012
+ {},
1013
+ '?merge_from_a=1000000000&merge_to_a=2000000000&selection_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{node="worker"}'
1014
+ ),
1015
+ });
1082
1016
 
1083
1017
  const {profileSelection} = result.current;
1084
1018
  expect(profileSelection).not.toBeNull();
@@ -1113,24 +1047,27 @@ describe('useQueryState', () => {
1113
1047
  });
1114
1048
 
1115
1049
  await waitFor(() => {
1116
- expect(mockNavigateTo).toHaveBeenCalled();
1050
+ expect(result1.current.profileSelection).not.toBeNull();
1117
1051
  });
1118
1052
 
1119
- const committedParams = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1][1];
1053
+ // Get the committed state values to build reload URL
1054
+ const historyParams = result1.current.profileSelection?.HistoryParams();
1055
+ const selectionA = historyParams?.selection ?? '';
1056
+ const mergeFromA = historyParams?.merge_from ?? '';
1057
+ const mergeToA = historyParams?.merge_to ?? '';
1120
1058
 
1121
- // Simulate page reload by updating mockLocation.search
1122
- const selectionA = String(committedParams.selection_a ?? '');
1123
- const mergeFromA = String(committedParams.merge_from_a ?? '');
1124
- const mergeToA = String(committedParams.merge_to_a ?? '');
1125
- mockLocation.search = `?selection_a=${encodeURIComponent(
1126
- selectionA
1127
- )}&merge_from_a=${mergeFromA}&merge_to_a=${mergeToA}`;
1128
1059
  unmount();
1129
1060
  mockNavigateTo.mockClear();
1130
1061
 
1131
- // Create new hook instance (simulating page reload)
1062
+ // Create new hook instance (simulating page reload) with the committed search params
1132
1063
  const {result: result2} = renderHook(() => useQueryState({suffix: '_a'}), {
1133
- wrapper: createWrapper(),
1064
+ wrapper: createWrapper(
1065
+ {},
1066
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1067
+ `?selection_a=${encodeURIComponent(selectionA)}&merge_from_a=${
1068
+ mergeFromA as string
1069
+ }&merge_to_a=${mergeToA as string}`
1070
+ ),
1134
1071
  });
1135
1072
 
1136
1073
  // Verify ProfileSelection is loaded from URL after reload
@@ -1139,13 +1076,12 @@ describe('useQueryState', () => {
1139
1076
 
1140
1077
  // Use interface methods to test
1141
1078
  expect(profileSelection?.Type()).toBe('merge');
1142
- const historyParams = profileSelection?.HistoryParams();
1143
- expect(historyParams?.merge_from).toBe('3000000000');
1144
- expect(historyParams?.merge_to).toBe('4000000000');
1145
- expect(historyParams?.selection).toBe('memory:alloc_objects:count:space:bytes{pod="test"}');
1146
-
1147
- // No navigation should occur on page load
1148
- expect(mockNavigateTo).not.toHaveBeenCalled();
1079
+ const reloadedHistoryParams = profileSelection?.HistoryParams();
1080
+ expect(reloadedHistoryParams?.merge_from).toBe('3000000000');
1081
+ expect(reloadedHistoryParams?.merge_to).toBe('4000000000');
1082
+ expect(reloadedHistoryParams?.selection).toBe(
1083
+ 'memory:alloc_objects:count:space:bytes{pod="test"}'
1084
+ );
1149
1085
  });
1150
1086
 
1151
1087
  it('should handle independent ProfileSelection for both sides in comparison mode', async () => {
@@ -1181,11 +1117,9 @@ describe('useQueryState', () => {
1181
1117
  });
1182
1118
 
1183
1119
  await waitFor(() => {
1184
- expect(mockNavigateTo).toHaveBeenCalled();
1120
+ expect(result.current.stateA.profileSelection).not.toBeNull();
1185
1121
  });
1186
1122
 
1187
- mockNavigateTo.mockClear();
1188
-
1189
1123
  // Set ProfileSelection for side B
1190
1124
  act(() => {
1191
1125
  result.current.stateB.setProfileSelection(
@@ -1195,19 +1129,7 @@ describe('useQueryState', () => {
1195
1129
  );
1196
1130
  });
1197
1131
 
1198
- await waitFor(() => {
1199
- expect(mockNavigateTo).toHaveBeenCalled();
1200
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
1201
-
1202
- // Both selections should be in URL with different suffixes
1203
- expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="app-a"}');
1204
- expect(params.selection_b).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="app-b"}');
1205
- expect(params.merge_from_a).toBe('1000000000');
1206
- expect(params.merge_from_b).toBe('3000000000');
1207
- });
1208
-
1209
- // The mockNavigateTo automatically updates mockLocation.search, so the URL change
1210
- // should propagate to the hooks automatically. Verify both ProfileSelections exist.
1132
+ // Verify both ProfileSelections exist
1211
1133
  await waitFor(() => {
1212
1134
  expect(result.current.stateA.profileSelection).not.toBeNull();
1213
1135
  expect(result.current.stateB.profileSelection).not.toBeNull();
@@ -1216,9 +1138,9 @@ describe('useQueryState', () => {
1216
1138
 
1217
1139
  it('should return null ProfileSelection when only partial params exist', () => {
1218
1140
  // Missing selection param
1219
- mockLocation.search = '?merge_from_a=1000000000&merge_to_a=2000000000';
1220
-
1221
- const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
1141
+ const {result} = renderHook(() => useQueryState({suffix: '_a'}), {
1142
+ wrapper: createWrapper({}, '?merge_from_a=1000000000&merge_to_a=2000000000'),
1143
+ });
1222
1144
 
1223
1145
  expect(result.current.profileSelection).toBeNull();
1224
1146
  });
@@ -1237,10 +1159,12 @@ describe('useQueryState', () => {
1237
1159
  });
1238
1160
 
1239
1161
  await waitFor(() => {
1240
- expect(mockNavigateTo).toHaveBeenCalled();
1241
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
1242
- expect(params.selection_a).toBe(
1243
- 'memory:alloc_objects:count:space:bytes:delta{namespace="default",pod="app-1",container="main"}'
1162
+ const {profileSelection} = result.current;
1163
+ expect(profileSelection).not.toBeNull();
1164
+ const historyParams = profileSelection?.HistoryParams();
1165
+ // The expression gets re-serialized through Query.parse which adds spaces after commas
1166
+ expect(historyParams?.selection).toBe(
1167
+ 'memory:alloc_objects:count:space:bytes:delta{namespace="default", pod="app-1", container="main"}'
1244
1168
  );
1245
1169
  });
1246
1170
  });
@@ -1259,20 +1183,24 @@ describe('useQueryState', () => {
1259
1183
  });
1260
1184
 
1261
1185
  await waitFor(() => {
1262
- // Should only navigate once despite setting 3 params (selection, merge_from, merge_to)
1263
- expect(mockNavigateTo).toHaveBeenCalledTimes(1);
1264
- const [, params] = mockNavigateTo.mock.calls[0];
1265
- expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}');
1266
- expect(params.merge_from_a).toBe('1000000000');
1267
- expect(params.merge_to_a).toBe('2000000000');
1186
+ const {profileSelection} = result.current;
1187
+ expect(profileSelection).not.toBeNull();
1188
+ const historyParams = profileSelection?.HistoryParams();
1189
+ expect(historyParams?.selection).toBe(
1190
+ 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}'
1191
+ );
1192
+ expect(historyParams?.merge_from).toBe('1000000000');
1193
+ expect(historyParams?.merge_to).toBe('2000000000');
1268
1194
  });
1269
1195
  });
1270
1196
 
1271
1197
  it('should preserve other URL params when setting ProfileSelection', async () => {
1272
- mockLocation.search =
1273
- '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&other_param=value&unrelated=test';
1274
-
1275
- const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
1198
+ const {result} = renderHook(() => useQueryState({suffix: '_a'}), {
1199
+ wrapper: createWrapper(
1200
+ {},
1201
+ '?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&other_param=value&unrelated=test'
1202
+ ),
1203
+ });
1276
1204
 
1277
1205
  const mockQuery = {
1278
1206
  toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}',
@@ -1284,16 +1212,18 @@ describe('useQueryState', () => {
1284
1212
  });
1285
1213
 
1286
1214
  await waitFor(() => {
1287
- expect(mockNavigateTo).toHaveBeenCalled();
1288
- const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
1289
-
1290
1215
  // ProfileSelection params should be set
1291
- expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}');
1216
+ const {profileSelection} = result.current;
1217
+ expect(profileSelection).not.toBeNull();
1218
+ const historyParams = profileSelection?.HistoryParams();
1219
+ expect(historyParams?.selection).toBe(
1220
+ 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}'
1221
+ );
1292
1222
 
1293
- // Other params should be preserved
1294
- expect(params.expression_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{}');
1295
- expect(params.other_param).toBe('value');
1296
- expect(params.unrelated).toBe('test');
1223
+ // Expression should still be present
1224
+ expect(result.current.querySelection.expression).toBe(
1225
+ 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{}'
1226
+ );
1297
1227
  });
1298
1228
  });
1299
1229
  });