@parca/profile 0.19.73 → 0.19.74
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.
- package/CHANGELOG.md +4 -0
- package/dist/MatchersInput/index.d.ts.map +1 -1
- package/dist/MatchersInput/index.js +4 -2
- package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts +1 -12
- package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts.map +1 -1
- package/dist/ProfileExplorer/ProfileExplorerCompare.js +52 -11
- package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts +1 -7
- package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts.map +1 -1
- package/dist/ProfileExplorer/ProfileExplorerSingle.js +4 -2
- package/dist/ProfileExplorer/index.d.ts +1 -4
- package/dist/ProfileExplorer/index.d.ts.map +1 -1
- package/dist/ProfileExplorer/index.js +11 -225
- package/dist/ProfileMetricsGraph/index.d.ts +1 -1
- package/dist/ProfileMetricsGraph/index.d.ts.map +1 -1
- package/dist/ProfileMetricsGraph/index.js +16 -20
- package/dist/ProfileSelector/MetricsGraphSection.d.ts +3 -3
- package/dist/ProfileSelector/MetricsGraphSection.d.ts.map +1 -1
- package/dist/ProfileSelector/MetricsGraphSection.js +10 -6
- package/dist/ProfileSelector/index.d.ts +2 -7
- package/dist/ProfileSelector/index.d.ts.map +1 -1
- package/dist/ProfileSelector/index.js +40 -46
- package/dist/ProfileSelector/useAutoQuerySelector.d.ts.map +1 -1
- package/dist/ProfileSelector/useAutoQuerySelector.js +19 -4
- package/dist/ProfileTypeSelector/index.d.ts.map +1 -1
- package/dist/ProfileTypeSelector/index.js +1 -1
- package/dist/ProfileView/components/ViewSelector/index.d.ts.map +1 -1
- package/dist/ProfileView/components/ViewSelector/index.js +10 -4
- package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.d.ts.map +1 -1
- package/dist/ProfileView/hooks/useResetStateOnProfileTypeChange.js +4 -2
- package/dist/ProfileView/hooks/useVisualizationState.d.ts.map +1 -1
- package/dist/ProfileView/hooks/useVisualizationState.js +20 -13
- package/dist/Table/MoreDropdown.d.ts.map +1 -1
- package/dist/Table/MoreDropdown.js +7 -3
- package/dist/Table/TableContextMenu.d.ts.map +1 -1
- package/dist/Table/TableContextMenu.js +9 -5
- package/dist/hooks/useCompareModeMeta.d.ts +10 -0
- package/dist/hooks/useCompareModeMeta.d.ts.map +1 -0
- package/dist/hooks/useCompareModeMeta.js +113 -0
- package/dist/hooks/useQueryState.d.ts +32 -0
- package/dist/hooks/useQueryState.d.ts.map +1 -0
- package/dist/hooks/useQueryState.js +285 -0
- package/dist/hooks/useQueryState.test.d.ts +2 -0
- package/dist/hooks/useQueryState.test.d.ts.map +1 -0
- package/dist/hooks/useQueryState.test.js +910 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/useSumBy.d.ts +7 -0
- package/dist/useSumBy.d.ts.map +1 -1
- package/dist/useSumBy.js +31 -6
- package/package.json +6 -6
- package/src/MatchersInput/index.tsx +4 -2
- package/src/ProfileExplorer/ProfileExplorerCompare.tsx +64 -46
- package/src/ProfileExplorer/ProfileExplorerSingle.tsx +7 -19
- package/src/ProfileExplorer/index.tsx +11 -339
- package/src/ProfileMetricsGraph/index.tsx +16 -20
- package/src/ProfileSelector/MetricsGraphSection.tsx +14 -10
- package/src/ProfileSelector/index.tsx +65 -83
- package/src/ProfileSelector/useAutoQuerySelector.ts +23 -5
- package/src/ProfileTypeSelector/index.tsx +3 -1
- package/src/ProfileView/components/ViewSelector/index.tsx +9 -4
- package/src/ProfileView/hooks/useResetStateOnProfileTypeChange.ts +4 -2
- package/src/ProfileView/hooks/useVisualizationState.ts +25 -12
- package/src/Table/MoreDropdown.tsx +7 -3
- package/src/Table/TableContextMenu.tsx +9 -5
- package/src/hooks/useCompareModeMeta.ts +131 -0
- package/src/hooks/useQueryState.test.tsx +1202 -0
- package/src/hooks/useQueryState.ts +414 -0
- package/src/index.tsx +9 -11
- package/src/useSumBy.ts +62 -7
- package/src/ProfileExplorer/index.test.ts +0 -97
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
// Copyright 2022 The Parca Authors
|
|
2
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
// you may not use this file except in compliance with the License.
|
|
4
|
+
// You may obtain a copy of the License at
|
|
5
|
+
//
|
|
6
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
7
|
+
//
|
|
8
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
9
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
10
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
11
|
+
// See the License for the specific language governing permissions and
|
|
12
|
+
// limitations under the License.
|
|
13
|
+
|
|
14
|
+
import {ReactNode} from 'react';
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line import/named
|
|
17
|
+
import {act, renderHook, waitFor} from '@testing-library/react';
|
|
18
|
+
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
|
19
|
+
|
|
20
|
+
import {URLStateProvider} from '@parca/components';
|
|
21
|
+
|
|
22
|
+
import {useQueryState} from './useQueryState';
|
|
23
|
+
|
|
24
|
+
// Mock window.location
|
|
25
|
+
const mockLocation = {
|
|
26
|
+
pathname: '/test',
|
|
27
|
+
search: '',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Mock the navigate function that actually updates the mock location
|
|
31
|
+
const mockNavigateTo = vi.fn((path: string, params: Record<string, string | string[]>) => {
|
|
32
|
+
// Convert params object to query string
|
|
33
|
+
const searchParams = new URLSearchParams();
|
|
34
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
35
|
+
if (value !== undefined && value !== null) {
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
// For arrays, join with commas
|
|
38
|
+
searchParams.set(key, value.join(','));
|
|
39
|
+
} else {
|
|
40
|
+
searchParams.set(key, String(value));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
mockLocation.search = `?${searchParams.toString()}`;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Mock the getQueryParamsFromURL function
|
|
48
|
+
vi.mock('@parca/components/src/hooks/URLState/utils', async () => {
|
|
49
|
+
const actual = await vi.importActual('@parca/components/src/hooks/URLState/utils');
|
|
50
|
+
return {
|
|
51
|
+
...actual,
|
|
52
|
+
getQueryParamsFromURL: () => {
|
|
53
|
+
if (mockLocation.search === '') return {};
|
|
54
|
+
const params = new URLSearchParams(mockLocation.search);
|
|
55
|
+
const result: Record<string, string | string[]> = {};
|
|
56
|
+
for (const [key, value] of params.entries()) {
|
|
57
|
+
const decodedValue = decodeURIComponent(value);
|
|
58
|
+
const existing = result[key];
|
|
59
|
+
if (existing !== undefined) {
|
|
60
|
+
result[key] = Array.isArray(existing)
|
|
61
|
+
? [...existing, decodedValue]
|
|
62
|
+
: [existing, decodedValue];
|
|
63
|
+
} else {
|
|
64
|
+
result[key] = decodedValue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Mock useSumBy to return the sumBy from URL params or undefined
|
|
73
|
+
vi.mock('../useSumBy', async () => {
|
|
74
|
+
const actual = await vi.importActual('../useSumBy');
|
|
75
|
+
return {
|
|
76
|
+
...actual,
|
|
77
|
+
useSumBy: (_queryClient: any, _profileType: any, _timeRange: any, defaultValue: any) => ({
|
|
78
|
+
sumBy: defaultValue,
|
|
79
|
+
setSumBy: vi.fn(),
|
|
80
|
+
isLoading: false,
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Helper to create wrapper with URLStateProvider
|
|
86
|
+
const createWrapper = (
|
|
87
|
+
paramPreferences = {}
|
|
88
|
+
): (({children}: {children: ReactNode}) => JSX.Element) => {
|
|
89
|
+
const Wrapper = ({children}: {children: ReactNode}): JSX.Element => (
|
|
90
|
+
<URLStateProvider navigateTo={mockNavigateTo} paramPreferences={paramPreferences}>
|
|
91
|
+
{children}
|
|
92
|
+
</URLStateProvider>
|
|
93
|
+
);
|
|
94
|
+
Wrapper.displayName = 'URLStateProviderWrapper';
|
|
95
|
+
return Wrapper;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
describe('useQueryState', () => {
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
mockNavigateTo.mockClear();
|
|
101
|
+
Object.defineProperty(window, 'location', {
|
|
102
|
+
value: mockLocation,
|
|
103
|
+
writable: true,
|
|
104
|
+
});
|
|
105
|
+
mockLocation.search = '';
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('Basic functionality', () => {
|
|
109
|
+
it('should initialize with default values', () => {
|
|
110
|
+
const {result} = renderHook(
|
|
111
|
+
() =>
|
|
112
|
+
useQueryState({
|
|
113
|
+
defaultExpression: 'process_cpu{}',
|
|
114
|
+
defaultTimeSelection: 'relative:hour|1',
|
|
115
|
+
defaultFrom: 1000,
|
|
116
|
+
defaultTo: 2000,
|
|
117
|
+
}),
|
|
118
|
+
{wrapper: createWrapper()}
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const {querySelection} = result.current;
|
|
122
|
+
expect(querySelection.expression).toBe('process_cpu{}');
|
|
123
|
+
expect(querySelection.timeSelection).toBe('relative:hour|1');
|
|
124
|
+
// From/to should be calculated from the range
|
|
125
|
+
expect(querySelection.from).toBeDefined();
|
|
126
|
+
expect(querySelection.to).toBeDefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should handle suffix for comparison mode', () => {
|
|
130
|
+
mockLocation.search =
|
|
131
|
+
'?expression_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from_a=1000&to_a=2000';
|
|
132
|
+
|
|
133
|
+
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
134
|
+
|
|
135
|
+
const {querySelection} = result.current;
|
|
136
|
+
expect(querySelection.expression).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}');
|
|
137
|
+
expect(querySelection.from).toBe(1000);
|
|
138
|
+
expect(querySelection.to).toBe(2000);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('Individual setters', () => {
|
|
143
|
+
it('should update expression and handle delta profiles', async () => {
|
|
144
|
+
const {result} = renderHook(
|
|
145
|
+
() =>
|
|
146
|
+
useQueryState({
|
|
147
|
+
defaultFrom: 1000,
|
|
148
|
+
defaultTo: 2000,
|
|
149
|
+
}),
|
|
150
|
+
{wrapper: createWrapper()}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
act(() => {
|
|
154
|
+
result.current.setDraftExpression('memory:alloc_objects:count:space:bytes:delta{}');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Draft should be updated but not committed
|
|
158
|
+
expect(result.current.draftSelection.expression).toBe(
|
|
159
|
+
'memory:alloc_objects:count:space:bytes:delta{}'
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Delta profile should auto-calculate merge params in draft
|
|
163
|
+
expect(result.current.draftSelection.mergeFrom).toBe('1000000000');
|
|
164
|
+
expect(result.current.draftSelection.mergeTo).toBe('2000000000');
|
|
165
|
+
|
|
166
|
+
act(() => {
|
|
167
|
+
result.current.commitDraft();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await waitFor(() => {
|
|
171
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
172
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
173
|
+
expect(params.expression).toBe('memory:alloc_objects:count:space:bytes:delta{}');
|
|
174
|
+
// Should set merge parameters for delta profile
|
|
175
|
+
expect(params).toHaveProperty('merge_from');
|
|
176
|
+
expect(params).toHaveProperty('merge_to');
|
|
177
|
+
expect(params.merge_from).toBe('1000000000');
|
|
178
|
+
expect(params.merge_to).toBe('2000000000');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('should update time range', async () => {
|
|
183
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
184
|
+
|
|
185
|
+
act(() => {
|
|
186
|
+
result.current.setDraftTimeRange(3000, 4000, 'relative:minute|5');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Draft should be updated
|
|
190
|
+
expect(result.current.draftSelection.from).toBe(3000);
|
|
191
|
+
expect(result.current.draftSelection.to).toBe(4000);
|
|
192
|
+
expect(result.current.draftSelection.timeSelection).toBe('relative:minute|5');
|
|
193
|
+
|
|
194
|
+
act(() => {
|
|
195
|
+
result.current.commitDraft();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
200
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
201
|
+
expect(params.from).toBe('3000');
|
|
202
|
+
expect(params.to).toBe('4000');
|
|
203
|
+
expect(params.time_selection).toBe('relative:minute|5');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should update sumBy', async () => {
|
|
208
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
209
|
+
|
|
210
|
+
act(() => {
|
|
211
|
+
result.current.setDraftSumBy(['namespace', 'container']);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Draft should be updated
|
|
215
|
+
expect(result.current.draftSelection.sumBy).toEqual(['namespace', 'container']);
|
|
216
|
+
|
|
217
|
+
act(() => {
|
|
218
|
+
result.current.commitDraft();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await waitFor(() => {
|
|
222
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
223
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
224
|
+
expect(params.sum_by).toBe('namespace,container');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should auto-calculate merge range for delta profiles', async () => {
|
|
229
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
230
|
+
|
|
231
|
+
// Set a delta profile expression
|
|
232
|
+
act(() => {
|
|
233
|
+
result.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
|
|
234
|
+
result.current.setDraftTimeRange(5000, 6000, 'relative:minute|5');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Merge range should be auto-calculated in draft
|
|
238
|
+
expect(result.current.draftSelection.mergeFrom).toBe('5000000000');
|
|
239
|
+
expect(result.current.draftSelection.mergeTo).toBe('6000000000');
|
|
240
|
+
|
|
241
|
+
act(() => {
|
|
242
|
+
result.current.commitDraft();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await waitFor(() => {
|
|
246
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
247
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
248
|
+
expect(params.merge_from).toBe('5000000000');
|
|
249
|
+
expect(params.merge_to).toBe('6000000000');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('Batch updates', () => {
|
|
255
|
+
it('should batch multiple updates into single navigation', async () => {
|
|
256
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
257
|
+
|
|
258
|
+
act(() => {
|
|
259
|
+
// Update multiple draft values
|
|
260
|
+
result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
|
|
261
|
+
result.current.setDraftTimeRange(7000, 8000, 'relative:minute|30');
|
|
262
|
+
result.current.setDraftSumBy(['pod', 'node']);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// All drafts should be updated
|
|
266
|
+
expect(result.current.draftSelection.expression).toBe(
|
|
267
|
+
'memory:inuse_space:bytes:space:bytes{}'
|
|
268
|
+
);
|
|
269
|
+
expect(result.current.draftSelection.from).toBe(7000);
|
|
270
|
+
expect(result.current.draftSelection.to).toBe(8000);
|
|
271
|
+
expect(result.current.draftSelection.sumBy).toEqual(['pod', 'node']);
|
|
272
|
+
|
|
273
|
+
act(() => {
|
|
274
|
+
result.current.commitDraft();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await waitFor(() => {
|
|
278
|
+
// Should only navigate once for all updates
|
|
279
|
+
expect(mockNavigateTo).toHaveBeenCalledTimes(1);
|
|
280
|
+
const [, params] = mockNavigateTo.mock.calls[0];
|
|
281
|
+
expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
|
|
282
|
+
expect(params.from).toBe('7000');
|
|
283
|
+
expect(params.to).toBe('8000');
|
|
284
|
+
expect(params.time_selection).toBe('relative:minute|30');
|
|
285
|
+
expect(params.sum_by).toBe('pod,node');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should handle partial updates', async () => {
|
|
290
|
+
mockLocation.search =
|
|
291
|
+
'?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000&time_selection=relative:hour|1';
|
|
292
|
+
|
|
293
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
294
|
+
|
|
295
|
+
act(() => {
|
|
296
|
+
// Only update expression, other values should remain
|
|
297
|
+
result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
expect(result.current.draftSelection.expression).toBe(
|
|
301
|
+
'memory:inuse_space:bytes:space:bytes{}'
|
|
302
|
+
);
|
|
303
|
+
// Other values should be from URL
|
|
304
|
+
expect(result.current.draftSelection.from).toBe(1000);
|
|
305
|
+
expect(result.current.draftSelection.to).toBe(2000);
|
|
306
|
+
|
|
307
|
+
act(() => {
|
|
308
|
+
result.current.commitDraft();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await waitFor(() => {
|
|
312
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
313
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
314
|
+
expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
|
|
315
|
+
expect(params.from).toBe('1000');
|
|
316
|
+
expect(params.to).toBe('2000');
|
|
317
|
+
expect(params.time_selection).toBe('relative:hour|1');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should auto-calculate merge params for delta profiles in batch update', async () => {
|
|
322
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
323
|
+
|
|
324
|
+
act(() => {
|
|
325
|
+
result.current.setDraftExpression('memory:alloc_space:bytes:space:bytes:delta{}');
|
|
326
|
+
result.current.setDraftTimeRange(9000, 10000, 'relative:minute|5');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Merge params should be auto-calculated in draft
|
|
330
|
+
expect(result.current.draftSelection.mergeFrom).toBe('9000000000');
|
|
331
|
+
expect(result.current.draftSelection.mergeTo).toBe('10000000000');
|
|
332
|
+
|
|
333
|
+
act(() => {
|
|
334
|
+
result.current.commitDraft();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await waitFor(() => {
|
|
338
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
339
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
340
|
+
expect(params.expression).toBe('memory:alloc_space:bytes:space:bytes:delta{}');
|
|
341
|
+
expect(params.merge_from).toBe('9000000000');
|
|
342
|
+
expect(params.merge_to).toBe('10000000000');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('Helper functions', () => {
|
|
348
|
+
it('should set profile name correctly', async () => {
|
|
349
|
+
mockLocation.search =
|
|
350
|
+
'?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{job="parca"}';
|
|
351
|
+
|
|
352
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
353
|
+
|
|
354
|
+
act(() => {
|
|
355
|
+
result.current.setDraftProfileName('memory:inuse_space:bytes:space:bytes');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Draft should be updated
|
|
359
|
+
expect(result.current.draftSelection.expression).toBe(
|
|
360
|
+
'memory:inuse_space:bytes:space:bytes{job="parca"}'
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
act(() => {
|
|
364
|
+
result.current.commitDraft();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
await waitFor(() => {
|
|
368
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
369
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
370
|
+
expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{job="parca"}');
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should set matchers correctly using draft', async () => {
|
|
375
|
+
mockLocation.search = '?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}';
|
|
376
|
+
|
|
377
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
378
|
+
|
|
379
|
+
act(() => {
|
|
380
|
+
result.current.setDraftMatchers('namespace="default",pod="my-pod"');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Draft should be updated but not URL yet
|
|
384
|
+
expect(result.current.draftSelection.expression).toBe(
|
|
385
|
+
'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{namespace="default",pod="my-pod"}'
|
|
386
|
+
);
|
|
387
|
+
expect(mockNavigateTo).not.toHaveBeenCalled();
|
|
388
|
+
|
|
389
|
+
// Commit the draft
|
|
390
|
+
act(() => {
|
|
391
|
+
result.current.commitDraft();
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
await waitFor(() => {
|
|
395
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
396
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
397
|
+
expect(params.expression).toBe(
|
|
398
|
+
'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{namespace="default",pod="my-pod"}'
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
describe('Comparison mode', () => {
|
|
405
|
+
it('should handle _a suffix correctly', async () => {
|
|
406
|
+
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
407
|
+
|
|
408
|
+
// Update draft state
|
|
409
|
+
act(() => {
|
|
410
|
+
result.current.setDraftExpression('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}');
|
|
411
|
+
result.current.setDraftTimeRange(1111, 2222, 'relative:hour|1');
|
|
412
|
+
result.current.setDraftSumBy(['label_a']);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Commit draft
|
|
416
|
+
act(() => {
|
|
417
|
+
result.current.commitDraft();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
await waitFor(() => {
|
|
421
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
422
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
423
|
+
expect(params.expression_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}');
|
|
424
|
+
expect(params.from_a).toBe('1111');
|
|
425
|
+
expect(params.to_a).toBe('2222');
|
|
426
|
+
expect(params.sum_by_a).toBe('label_a');
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should handle _b suffix correctly', async () => {
|
|
431
|
+
const {result} = renderHook(() => useQueryState({suffix: '_b'}), {wrapper: createWrapper()});
|
|
432
|
+
|
|
433
|
+
// Update draft state
|
|
434
|
+
act(() => {
|
|
435
|
+
result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
|
|
436
|
+
result.current.setDraftTimeRange(3333, 4444, 'relative:hour|2');
|
|
437
|
+
result.current.setDraftSumBy(['label_b']);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// Commit draft
|
|
441
|
+
act(() => {
|
|
442
|
+
result.current.commitDraft();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await waitFor(() => {
|
|
446
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
447
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
448
|
+
expect(params.expression_b).toBe('memory:inuse_space:bytes:space:bytes{}');
|
|
449
|
+
expect(params.from_b).toBe('3333');
|
|
450
|
+
expect(params.to_b).toBe('4444');
|
|
451
|
+
expect(params.sum_by_b).toBe('label_b');
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
describe('Draft state pattern', () => {
|
|
457
|
+
it('should not update URL until commit', async () => {
|
|
458
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
459
|
+
|
|
460
|
+
// Make multiple draft changes
|
|
461
|
+
act(() => {
|
|
462
|
+
result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
|
|
463
|
+
result.current.setDraftTimeRange(5000, 6000, 'relative:hour|3');
|
|
464
|
+
result.current.setDraftSumBy(['namespace', 'pod']);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// URL should not be updated yet
|
|
468
|
+
expect(mockNavigateTo).not.toHaveBeenCalled();
|
|
469
|
+
|
|
470
|
+
// Commit all changes at once
|
|
471
|
+
act(() => {
|
|
472
|
+
result.current.commitDraft();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Now URL should be updated exactly once with all changes
|
|
476
|
+
await waitFor(() => {
|
|
477
|
+
expect(mockNavigateTo).toHaveBeenCalledTimes(1);
|
|
478
|
+
const [, params] = mockNavigateTo.mock.calls[0];
|
|
479
|
+
expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
|
|
480
|
+
expect(params.from).toBe('5000');
|
|
481
|
+
expect(params.to).toBe('6000');
|
|
482
|
+
expect(params.sum_by).toBe('namespace,pod');
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should handle draft profile name changes', () => {
|
|
487
|
+
mockLocation.search =
|
|
488
|
+
'?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{job="test"}';
|
|
489
|
+
|
|
490
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
491
|
+
|
|
492
|
+
// Change profile name in draft
|
|
493
|
+
act(() => {
|
|
494
|
+
result.current.setDraftProfileName('memory:inuse_space:bytes:space:bytes');
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Draft should be updated
|
|
498
|
+
expect(result.current.draftSelection.expression).toBe(
|
|
499
|
+
'memory:inuse_space:bytes:space:bytes{job="test"}'
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
// URL should not be updated yet
|
|
503
|
+
expect(mockNavigateTo).not.toHaveBeenCalled();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
describe('Edge cases', () => {
|
|
508
|
+
it('should handle invalid expression gracefully', () => {
|
|
509
|
+
const {result} = renderHook(
|
|
510
|
+
() =>
|
|
511
|
+
useQueryState({
|
|
512
|
+
defaultExpression: 'invalid{{}expression',
|
|
513
|
+
}),
|
|
514
|
+
{wrapper: createWrapper()}
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
// Should not throw error
|
|
518
|
+
expect(() => result.current.querySelection).not.toThrow();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('should clear merge params for non-delta profiles', async () => {
|
|
522
|
+
mockLocation.search =
|
|
523
|
+
'?expression=memory:alloc_objects:count:space:bytes:delta{}&merge_from=1000000000&merge_to=2000000000';
|
|
524
|
+
|
|
525
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
526
|
+
|
|
527
|
+
// Switch to non-delta profile (without :delta suffix) using draft
|
|
528
|
+
act(() => {
|
|
529
|
+
result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Commit the draft
|
|
533
|
+
act(() => {
|
|
534
|
+
result.current.commitDraft();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
await waitFor(() => {
|
|
538
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
539
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
540
|
+
expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
|
|
541
|
+
expect(params).not.toHaveProperty('merge_from');
|
|
542
|
+
expect(params).not.toHaveProperty('merge_to');
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it('should preserve other URL parameters when updating', async () => {
|
|
547
|
+
mockLocation.search =
|
|
548
|
+
'?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&other_param=value&unrelated=test';
|
|
549
|
+
|
|
550
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
551
|
+
|
|
552
|
+
// Update draft and commit
|
|
553
|
+
act(() => {
|
|
554
|
+
result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
act(() => {
|
|
558
|
+
result.current.commitDraft();
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
await waitFor(() => {
|
|
562
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
563
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
564
|
+
expect(params.expression).toBe('memory:inuse_space:bytes:space:bytes{}');
|
|
565
|
+
expect(params.other_param).toBe('value');
|
|
566
|
+
expect(params.unrelated).toBe('test');
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
describe('Commit with refreshed time range (time range re-evaluation)', () => {
|
|
572
|
+
it('should use refreshed time range values instead of draft state when provided', async () => {
|
|
573
|
+
mockLocation.search =
|
|
574
|
+
'?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&from=1000&to=2000&time_selection=relative:minute|15';
|
|
575
|
+
|
|
576
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
577
|
+
|
|
578
|
+
// Draft state has original values
|
|
579
|
+
expect(result.current.draftSelection.from).toBe(1000);
|
|
580
|
+
expect(result.current.draftSelection.to).toBe(2000);
|
|
581
|
+
expect(result.current.draftSelection.timeSelection).toBe('relative:minute|15');
|
|
582
|
+
|
|
583
|
+
// Commit with refreshed time range (simulating re-evaluated time range)
|
|
584
|
+
act(() => {
|
|
585
|
+
result.current.commitDraft({
|
|
586
|
+
from: 5000,
|
|
587
|
+
to: 6000,
|
|
588
|
+
timeSelection: 'relative:minute|15',
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
await waitFor(() => {
|
|
593
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
594
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
595
|
+
// Should use refreshed time range values, not draft values
|
|
596
|
+
expect(params.from).toBe('5000');
|
|
597
|
+
expect(params.to).toBe('6000');
|
|
598
|
+
expect(params.time_selection).toBe('relative:minute|15');
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('should update draft state with refreshed time range after commit', async () => {
|
|
603
|
+
const {result} = renderHook(
|
|
604
|
+
() =>
|
|
605
|
+
useQueryState({
|
|
606
|
+
defaultExpression: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}',
|
|
607
|
+
defaultFrom: 1000,
|
|
608
|
+
defaultTo: 2000,
|
|
609
|
+
defaultTimeSelection: 'relative:minute|5',
|
|
610
|
+
}),
|
|
611
|
+
{wrapper: createWrapper()}
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
// Commit with refreshed time values
|
|
615
|
+
act(() => {
|
|
616
|
+
result.current.commitDraft({
|
|
617
|
+
from: 3000,
|
|
618
|
+
to: 4000,
|
|
619
|
+
timeSelection: 'relative:minute|5',
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
await waitFor(() => {
|
|
624
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// Draft state should be updated with the refreshed time range
|
|
628
|
+
expect(result.current.draftSelection.from).toBe(3000);
|
|
629
|
+
expect(result.current.draftSelection.to).toBe(4000);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('should trigger navigation even when expression unchanged (time re-evaluation)', async () => {
|
|
633
|
+
mockLocation.search =
|
|
634
|
+
'?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&from=1000&to=2000&time_selection=relative:minute|5';
|
|
635
|
+
|
|
636
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
637
|
+
|
|
638
|
+
mockNavigateTo.mockClear();
|
|
639
|
+
|
|
640
|
+
// First commit with new time values
|
|
641
|
+
act(() => {
|
|
642
|
+
result.current.commitDraft({
|
|
643
|
+
from: 5000,
|
|
644
|
+
to: 6000,
|
|
645
|
+
timeSelection: 'relative:minute|5',
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
await waitFor(() => {
|
|
650
|
+
expect(mockNavigateTo).toHaveBeenCalledTimes(1);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const firstCallParams = mockNavigateTo.mock.calls[0][1];
|
|
654
|
+
expect(firstCallParams.from).toBe('5000');
|
|
655
|
+
expect(firstCallParams.to).toBe('6000');
|
|
656
|
+
|
|
657
|
+
mockNavigateTo.mockClear();
|
|
658
|
+
|
|
659
|
+
// Second commit with different time values (simulating clicking Search again)
|
|
660
|
+
act(() => {
|
|
661
|
+
result.current.commitDraft({
|
|
662
|
+
from: 7000,
|
|
663
|
+
to: 8000,
|
|
664
|
+
timeSelection: 'relative:minute|5',
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
await waitFor(() => {
|
|
669
|
+
expect(mockNavigateTo).toHaveBeenCalledTimes(1);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
const secondCallParams = mockNavigateTo.mock.calls[0][1];
|
|
673
|
+
expect(secondCallParams.from).toBe('7000');
|
|
674
|
+
expect(secondCallParams.to).toBe('8000');
|
|
675
|
+
|
|
676
|
+
// Verify that navigation was called both times despite expression being unchanged
|
|
677
|
+
expect(firstCallParams.from).not.toBe(secondCallParams.from);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it('should auto-calculate merge params for delta profiles when using refreshed time range', async () => {
|
|
681
|
+
const {result} = renderHook(
|
|
682
|
+
() =>
|
|
683
|
+
useQueryState({
|
|
684
|
+
defaultExpression: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}',
|
|
685
|
+
defaultFrom: 1000,
|
|
686
|
+
defaultTo: 2000,
|
|
687
|
+
}),
|
|
688
|
+
{wrapper: createWrapper()}
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
// Commit with refreshed time range for delta profile
|
|
692
|
+
act(() => {
|
|
693
|
+
result.current.commitDraft({
|
|
694
|
+
from: 5000,
|
|
695
|
+
to: 6000,
|
|
696
|
+
timeSelection: 'relative:minute|5',
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
await waitFor(() => {
|
|
701
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
702
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
703
|
+
|
|
704
|
+
// Verify merge params are calculated from refreshed time range
|
|
705
|
+
expect(params.merge_from).toBe('5000000000'); // 5000ms * 1_000_000
|
|
706
|
+
expect(params.merge_to).toBe('6000000000'); // 6000ms * 1_000_000
|
|
707
|
+
});
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('should use draft values when refreshedTimeRange is not provided', async () => {
|
|
711
|
+
mockLocation.search =
|
|
712
|
+
'?expression=memory:inuse_space:bytes:space:bytes{}&from=1000&to=2000&time_selection=relative:hour|1';
|
|
713
|
+
|
|
714
|
+
const {result} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
715
|
+
|
|
716
|
+
// Change draft values
|
|
717
|
+
act(() => {
|
|
718
|
+
result.current.setDraftTimeRange(3000, 4000, 'relative:minute|30');
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// Commit without refreshedTimeRange - should use draft values
|
|
722
|
+
act(() => {
|
|
723
|
+
result.current.commitDraft();
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
await waitFor(() => {
|
|
727
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
728
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
729
|
+
|
|
730
|
+
// Should use updated draft values
|
|
731
|
+
expect(params.from).toBe('3000');
|
|
732
|
+
expect(params.to).toBe('4000');
|
|
733
|
+
expect(params.time_selection).toBe('relative:minute|30');
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
describe('State persistence after page reload', () => {
|
|
739
|
+
it('should retain committed values after page reload simulation', async () => {
|
|
740
|
+
// Initial state
|
|
741
|
+
mockLocation.search =
|
|
742
|
+
'?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds{}&from=1000&to=2000';
|
|
743
|
+
|
|
744
|
+
const {result: result1, unmount} = renderHook(() => useQueryState(), {
|
|
745
|
+
wrapper: createWrapper(),
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
// User makes changes to draft
|
|
749
|
+
act(() => {
|
|
750
|
+
result1.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
|
|
751
|
+
result1.current.setDraftTimeRange(5000, 6000, 'relative:minute|15');
|
|
752
|
+
result1.current.setDraftSumBy(['namespace', 'pod']);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
// User clicks Search to commit
|
|
756
|
+
act(() => {
|
|
757
|
+
result1.current.commitDraft();
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
await waitFor(() => {
|
|
761
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
// Get the params that were committed to URL
|
|
765
|
+
const committedParams = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1][1];
|
|
766
|
+
|
|
767
|
+
// Simulate page reload by updating mockLocation.search with committed values
|
|
768
|
+
const queryString = new URLSearchParams({
|
|
769
|
+
expression: committedParams.expression as string,
|
|
770
|
+
from: committedParams.from as string,
|
|
771
|
+
to: committedParams.to as string,
|
|
772
|
+
time_selection: committedParams.time_selection as string,
|
|
773
|
+
sum_by: committedParams.sum_by as string,
|
|
774
|
+
}).toString();
|
|
775
|
+
|
|
776
|
+
mockLocation.search = `?${queryString}`;
|
|
777
|
+
|
|
778
|
+
// Unmount the old hook instance
|
|
779
|
+
unmount();
|
|
780
|
+
|
|
781
|
+
// Clear navigation mock to verify no new navigation on reload
|
|
782
|
+
mockNavigateTo.mockClear();
|
|
783
|
+
|
|
784
|
+
// Create new hook instance (simulating page reload)
|
|
785
|
+
const {result: result2} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
786
|
+
|
|
787
|
+
// Verify state is loaded from URL after "reload"
|
|
788
|
+
expect(result2.current.querySelection.expression).toBe(
|
|
789
|
+
'memory:inuse_space:bytes:space:bytes{}'
|
|
790
|
+
);
|
|
791
|
+
expect(result2.current.querySelection.from).toBe(5000);
|
|
792
|
+
expect(result2.current.querySelection.to).toBe(6000);
|
|
793
|
+
expect(result2.current.querySelection.timeSelection).toBe('relative:minute|15');
|
|
794
|
+
expect(result2.current.querySelection.sumBy).toEqual(['namespace', 'pod']);
|
|
795
|
+
|
|
796
|
+
// Draft should be synced with URL state on page load
|
|
797
|
+
expect(result2.current.draftSelection.expression).toBe(
|
|
798
|
+
'memory:inuse_space:bytes:space:bytes{}'
|
|
799
|
+
);
|
|
800
|
+
expect(result2.current.draftSelection.from).toBe(5000);
|
|
801
|
+
expect(result2.current.draftSelection.to).toBe(6000);
|
|
802
|
+
expect(result2.current.draftSelection.sumBy).toEqual(['namespace', 'pod']);
|
|
803
|
+
|
|
804
|
+
// No navigation should occur on page load
|
|
805
|
+
expect(mockNavigateTo).not.toHaveBeenCalled();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it('should preserve delta profile merge params after reload', async () => {
|
|
809
|
+
// Initial state with delta profile
|
|
810
|
+
mockLocation.search =
|
|
811
|
+
'?expression=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{}&from=1000&to=2000';
|
|
812
|
+
|
|
813
|
+
const {result: result1, unmount} = renderHook(() => useQueryState(), {
|
|
814
|
+
wrapper: createWrapper(),
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
// Commit with time override
|
|
818
|
+
act(() => {
|
|
819
|
+
result1.current.commitDraft({
|
|
820
|
+
from: 5000,
|
|
821
|
+
to: 6000,
|
|
822
|
+
timeSelection: 'relative:minute|5',
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
await waitFor(() => {
|
|
827
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
const committedParams = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1][1];
|
|
831
|
+
|
|
832
|
+
// Verify merge params were set
|
|
833
|
+
expect(committedParams.merge_from).toBe('5000000000');
|
|
834
|
+
expect(committedParams.merge_to).toBe('6000000000');
|
|
835
|
+
|
|
836
|
+
// Simulate page reload with all params including merge params
|
|
837
|
+
const queryString = new URLSearchParams({
|
|
838
|
+
expression: committedParams.expression as string,
|
|
839
|
+
from: committedParams.from as string,
|
|
840
|
+
to: committedParams.to as string,
|
|
841
|
+
time_selection: committedParams.time_selection as string,
|
|
842
|
+
merge_from: committedParams.merge_from as string,
|
|
843
|
+
merge_to: committedParams.merge_to as string,
|
|
844
|
+
}).toString();
|
|
845
|
+
|
|
846
|
+
mockLocation.search = `?${queryString}`;
|
|
847
|
+
unmount();
|
|
848
|
+
mockNavigateTo.mockClear();
|
|
849
|
+
|
|
850
|
+
// Create new hook instance
|
|
851
|
+
const {result: result2} = renderHook(() => useQueryState(), {wrapper: createWrapper()});
|
|
852
|
+
|
|
853
|
+
// Verify merge params are preserved
|
|
854
|
+
expect(result2.current.querySelection.mergeFrom).toBe('5000000000');
|
|
855
|
+
expect(result2.current.querySelection.mergeTo).toBe('6000000000');
|
|
856
|
+
|
|
857
|
+
// Draft should also have merge params
|
|
858
|
+
expect(result2.current.draftSelection.mergeFrom).toBe('5000000000');
|
|
859
|
+
expect(result2.current.draftSelection.mergeTo).toBe('6000000000');
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
describe('ProfileSelection state management', () => {
|
|
864
|
+
it('should initialize with null ProfileSelection when no URL params exist', () => {
|
|
865
|
+
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
866
|
+
|
|
867
|
+
expect(result.current.profileSelection).toBeNull();
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('should compute ProfileSelection from URL params', () => {
|
|
871
|
+
// Set URL with ProfileSelection params - using valid profile type
|
|
872
|
+
mockLocation.search =
|
|
873
|
+
'?merge_from_a=1234567890&merge_to_a=9876543210&selection_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{pod="test"}';
|
|
874
|
+
|
|
875
|
+
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
876
|
+
|
|
877
|
+
const {profileSelection} = result.current;
|
|
878
|
+
expect(profileSelection).not.toBeNull();
|
|
879
|
+
|
|
880
|
+
// Test using the interface methods
|
|
881
|
+
expect(profileSelection?.Type()).toBe('merge');
|
|
882
|
+
expect(profileSelection?.ProfileName()).toBe(
|
|
883
|
+
'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta'
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
// Test HistoryParams which should return merge params
|
|
887
|
+
const historyParams = profileSelection?.HistoryParams();
|
|
888
|
+
expect(historyParams?.merge_from).toBe('1234567890');
|
|
889
|
+
expect(historyParams?.merge_to).toBe('9876543210');
|
|
890
|
+
expect(historyParams?.selection).toBe(
|
|
891
|
+
'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{pod="test"}'
|
|
892
|
+
);
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('should auto-commit ProfileSelection to URL when setProfileSelection called', async () => {
|
|
896
|
+
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
897
|
+
|
|
898
|
+
const mergeFrom = BigInt(5000000000);
|
|
899
|
+
const mergeTo = BigInt(6000000000);
|
|
900
|
+
|
|
901
|
+
// Create a mock Query object - in real code, this would be Query.parse()
|
|
902
|
+
const mockQuery = {
|
|
903
|
+
toString: () => 'memory:inuse_space:bytes:space:bytes{namespace="default"}',
|
|
904
|
+
profileType: () => ({delta: false}),
|
|
905
|
+
} as any;
|
|
906
|
+
|
|
907
|
+
act(() => {
|
|
908
|
+
result.current.setProfileSelection(mergeFrom, mergeTo, mockQuery);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
await waitFor(() => {
|
|
912
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
913
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
914
|
+
expect(params.selection_a).toBe(
|
|
915
|
+
'memory:inuse_space:bytes:space:bytes{namespace="default"}'
|
|
916
|
+
);
|
|
917
|
+
expect(params.merge_from_a).toBe('5000000000');
|
|
918
|
+
expect(params.merge_to_a).toBe('6000000000');
|
|
919
|
+
});
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it('should use correct suffix for ProfileSelection in comparison mode', async () => {
|
|
923
|
+
const {result: resultB} = renderHook(() => useQueryState({suffix: '_b'}), {
|
|
924
|
+
wrapper: createWrapper(),
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
const mergeFrom = BigInt(7000000000);
|
|
928
|
+
const mergeTo = BigInt(8000000000);
|
|
929
|
+
|
|
930
|
+
const mockQuery = {
|
|
931
|
+
toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}',
|
|
932
|
+
profileType: () => ({delta: false}),
|
|
933
|
+
} as any;
|
|
934
|
+
|
|
935
|
+
act(() => {
|
|
936
|
+
resultB.current.setProfileSelection(mergeFrom, mergeTo, mockQuery);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
await waitFor(() => {
|
|
940
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
941
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
942
|
+
expect(params.selection_b).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}');
|
|
943
|
+
expect(params.merge_from_b).toBe('7000000000');
|
|
944
|
+
expect(params.merge_to_b).toBe('8000000000');
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('should clear ProfileSelection when commitDraft is called', async () => {
|
|
949
|
+
// Start with a ProfileSelection in URL - using valid profile type
|
|
950
|
+
mockLocation.search =
|
|
951
|
+
'?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"}';
|
|
952
|
+
|
|
953
|
+
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
954
|
+
|
|
955
|
+
// Verify ProfileSelection exists
|
|
956
|
+
expect(result.current.profileSelection).not.toBeNull();
|
|
957
|
+
|
|
958
|
+
// Make a change to trigger commit
|
|
959
|
+
act(() => {
|
|
960
|
+
result.current.setDraftExpression('memory:inuse_space:bytes:space:bytes{}');
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// Commit the draft (this should clear ProfileSelection as per design decision 4.B)
|
|
964
|
+
act(() => {
|
|
965
|
+
result.current.commitDraft();
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
await waitFor(() => {
|
|
969
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
970
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
971
|
+
|
|
972
|
+
// ProfileSelection params should be cleared
|
|
973
|
+
expect(params).not.toHaveProperty('selection_a');
|
|
974
|
+
|
|
975
|
+
// But QuerySelection params should still be present
|
|
976
|
+
expect(params.expression_a).toBe('memory:inuse_space:bytes:space:bytes{}');
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
it('should handle ProfileSelection with delta profiles correctly', () => {
|
|
981
|
+
mockLocation.search =
|
|
982
|
+
'?merge_from_a=1000000000&merge_to_a=2000000000&selection_a=process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta{node="worker"}';
|
|
983
|
+
|
|
984
|
+
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
985
|
+
|
|
986
|
+
const {profileSelection} = result.current;
|
|
987
|
+
expect(profileSelection).not.toBeNull();
|
|
988
|
+
|
|
989
|
+
// Test that ProfileSelection recognizes delta profile type
|
|
990
|
+
expect(profileSelection?.ProfileName()).toBe(
|
|
991
|
+
'process_cpu:cpu:nanoseconds:cpu:nanoseconds:delta'
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
// Test HistoryParams
|
|
995
|
+
const historyParams = profileSelection?.HistoryParams();
|
|
996
|
+
expect(historyParams?.merge_from).toBe('1000000000');
|
|
997
|
+
expect(historyParams?.merge_to).toBe('2000000000');
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
it('should persist ProfileSelection across page reloads', async () => {
|
|
1001
|
+
// Initial state - user clicks on metrics graph point
|
|
1002
|
+
const {result: result1, unmount} = renderHook(() => useQueryState({suffix: '_a'}), {
|
|
1003
|
+
wrapper: createWrapper(),
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
const mergeFrom = BigInt(3000000000);
|
|
1007
|
+
const mergeTo = BigInt(4000000000);
|
|
1008
|
+
const mockQuery = {
|
|
1009
|
+
toString: () => 'memory:alloc_objects:count:space:bytes{pod="test"}',
|
|
1010
|
+
profileType: () => ({delta: false}),
|
|
1011
|
+
} as any;
|
|
1012
|
+
|
|
1013
|
+
// Set ProfileSelection
|
|
1014
|
+
act(() => {
|
|
1015
|
+
result1.current.setProfileSelection(mergeFrom, mergeTo, mockQuery);
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
await waitFor(() => {
|
|
1019
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
const committedParams = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1][1];
|
|
1023
|
+
|
|
1024
|
+
// Simulate page reload by updating mockLocation.search
|
|
1025
|
+
const selectionA = String(committedParams.selection_a ?? '');
|
|
1026
|
+
const mergeFromA = String(committedParams.merge_from_a ?? '');
|
|
1027
|
+
const mergeToA = String(committedParams.merge_to_a ?? '');
|
|
1028
|
+
mockLocation.search = `?selection_a=${encodeURIComponent(
|
|
1029
|
+
selectionA
|
|
1030
|
+
)}&merge_from_a=${mergeFromA}&merge_to_a=${mergeToA}`;
|
|
1031
|
+
unmount();
|
|
1032
|
+
mockNavigateTo.mockClear();
|
|
1033
|
+
|
|
1034
|
+
// Create new hook instance (simulating page reload)
|
|
1035
|
+
const {result: result2} = renderHook(() => useQueryState({suffix: '_a'}), {
|
|
1036
|
+
wrapper: createWrapper(),
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
// Verify ProfileSelection is loaded from URL after reload
|
|
1040
|
+
const profileSelection = result2.current.profileSelection;
|
|
1041
|
+
expect(profileSelection).not.toBeNull();
|
|
1042
|
+
|
|
1043
|
+
// Use interface methods to test
|
|
1044
|
+
expect(profileSelection?.Type()).toBe('merge');
|
|
1045
|
+
const historyParams = profileSelection?.HistoryParams();
|
|
1046
|
+
expect(historyParams?.merge_from).toBe('3000000000');
|
|
1047
|
+
expect(historyParams?.merge_to).toBe('4000000000');
|
|
1048
|
+
expect(historyParams?.selection).toBe('memory:alloc_objects:count:space:bytes{pod="test"}');
|
|
1049
|
+
|
|
1050
|
+
// No navigation should occur on page load
|
|
1051
|
+
expect(mockNavigateTo).not.toHaveBeenCalled();
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it('should handle independent ProfileSelection for both sides in comparison mode', async () => {
|
|
1055
|
+
// Test component using both hooks with the same URLStateProvider (real-world scenario)
|
|
1056
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
1057
|
+
const TestComponent = () => {
|
|
1058
|
+
const stateA = useQueryState({suffix: '_a'});
|
|
1059
|
+
const stateB = useQueryState({suffix: '_b'});
|
|
1060
|
+
return {stateA, stateB};
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
const {result} = renderHook(() => TestComponent(), {
|
|
1064
|
+
wrapper: createWrapper(),
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
const mockQueryA = {
|
|
1068
|
+
toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="app-a"}',
|
|
1069
|
+
profileType: () => ({delta: false}),
|
|
1070
|
+
} as any;
|
|
1071
|
+
|
|
1072
|
+
const mockQueryB = {
|
|
1073
|
+
toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="app-b"}',
|
|
1074
|
+
profileType: () => ({delta: false}),
|
|
1075
|
+
} as any;
|
|
1076
|
+
|
|
1077
|
+
// Set ProfileSelection for side A
|
|
1078
|
+
act(() => {
|
|
1079
|
+
result.current.stateA.setProfileSelection(
|
|
1080
|
+
BigInt(1000000000),
|
|
1081
|
+
BigInt(2000000000),
|
|
1082
|
+
mockQueryA
|
|
1083
|
+
);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
await waitFor(() => {
|
|
1087
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
mockNavigateTo.mockClear();
|
|
1091
|
+
|
|
1092
|
+
// Set ProfileSelection for side B
|
|
1093
|
+
act(() => {
|
|
1094
|
+
result.current.stateB.setProfileSelection(
|
|
1095
|
+
BigInt(3000000000),
|
|
1096
|
+
BigInt(4000000000),
|
|
1097
|
+
mockQueryB
|
|
1098
|
+
);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
await waitFor(() => {
|
|
1102
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
1103
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
1104
|
+
|
|
1105
|
+
// Both selections should be in URL with different suffixes
|
|
1106
|
+
expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="app-a"}');
|
|
1107
|
+
expect(params.selection_b).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="app-b"}');
|
|
1108
|
+
expect(params.merge_from_a).toBe('1000000000');
|
|
1109
|
+
expect(params.merge_from_b).toBe('3000000000');
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
// The mockNavigateTo automatically updates mockLocation.search, so the URL change
|
|
1113
|
+
// should propagate to the hooks automatically. Verify both ProfileSelections exist.
|
|
1114
|
+
await waitFor(() => {
|
|
1115
|
+
expect(result.current.stateA.profileSelection).not.toBeNull();
|
|
1116
|
+
expect(result.current.stateB.profileSelection).not.toBeNull();
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
it('should return null ProfileSelection when only partial params exist', () => {
|
|
1121
|
+
// Missing selection param
|
|
1122
|
+
mockLocation.search = '?merge_from_a=1000000000&merge_to_a=2000000000';
|
|
1123
|
+
|
|
1124
|
+
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
1125
|
+
|
|
1126
|
+
expect(result.current.profileSelection).toBeNull();
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it('should handle ProfileSelection with complex query expressions', async () => {
|
|
1130
|
+
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
1131
|
+
|
|
1132
|
+
const mockQuery = {
|
|
1133
|
+
toString: () =>
|
|
1134
|
+
'memory:alloc_objects:count:space:bytes:delta{namespace="default",pod="app-1",container="main"}',
|
|
1135
|
+
profileType: () => ({delta: true}),
|
|
1136
|
+
} as any;
|
|
1137
|
+
|
|
1138
|
+
act(() => {
|
|
1139
|
+
result.current.setProfileSelection(BigInt(5000000000), BigInt(6000000000), mockQuery);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
await waitFor(() => {
|
|
1143
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
1144
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
1145
|
+
expect(params.selection_a).toBe(
|
|
1146
|
+
'memory:alloc_objects:count:space:bytes:delta{namespace="default",pod="app-1",container="main"}'
|
|
1147
|
+
);
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
it('should batch ProfileSelection update with other URL state changes', async () => {
|
|
1152
|
+
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
1153
|
+
|
|
1154
|
+
const mockQuery = {
|
|
1155
|
+
toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}',
|
|
1156
|
+
profileType: () => ({delta: false}),
|
|
1157
|
+
} as any;
|
|
1158
|
+
|
|
1159
|
+
// The batchUpdates is used internally by setProfileSelection
|
|
1160
|
+
act(() => {
|
|
1161
|
+
result.current.setProfileSelection(BigInt(1000000000), BigInt(2000000000), mockQuery);
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
await waitFor(() => {
|
|
1165
|
+
// Should only navigate once despite setting 3 params (selection, merge_from, merge_to)
|
|
1166
|
+
expect(mockNavigateTo).toHaveBeenCalledTimes(1);
|
|
1167
|
+
const [, params] = mockNavigateTo.mock.calls[0];
|
|
1168
|
+
expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{job="test"}');
|
|
1169
|
+
expect(params.merge_from_a).toBe('1000000000');
|
|
1170
|
+
expect(params.merge_to_a).toBe('2000000000');
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
it('should preserve other URL params when setting ProfileSelection', async () => {
|
|
1175
|
+
mockLocation.search = '?expression_a=process_cpu{}&other_param=value&unrelated=test';
|
|
1176
|
+
|
|
1177
|
+
const {result} = renderHook(() => useQueryState({suffix: '_a'}), {wrapper: createWrapper()});
|
|
1178
|
+
|
|
1179
|
+
const mockQuery = {
|
|
1180
|
+
toString: () => 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}',
|
|
1181
|
+
profileType: () => ({delta: false}),
|
|
1182
|
+
} as any;
|
|
1183
|
+
|
|
1184
|
+
act(() => {
|
|
1185
|
+
result.current.setProfileSelection(BigInt(1000000000), BigInt(2000000000), mockQuery);
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
await waitFor(() => {
|
|
1189
|
+
expect(mockNavigateTo).toHaveBeenCalled();
|
|
1190
|
+
const [, params] = mockNavigateTo.mock.calls[mockNavigateTo.mock.calls.length - 1];
|
|
1191
|
+
|
|
1192
|
+
// ProfileSelection params should be set
|
|
1193
|
+
expect(params.selection_a).toBe('process_cpu:cpu:nanoseconds:cpu:nanoseconds{pod="test"}');
|
|
1194
|
+
|
|
1195
|
+
// Other params should be preserved
|
|
1196
|
+
expect(params.expression_a).toBe('process_cpu{}');
|
|
1197
|
+
expect(params.other_param).toBe('value');
|
|
1198
|
+
expect(params.unrelated).toBe('test');
|
|
1199
|
+
});
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
});
|