datajunction-ui 0.0.43 → 0.0.45
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/package.json +1 -1
- package/src/app/components/__tests__/NamespaceHeader.test.jsx +349 -1
- package/src/app/pages/NodePage/NodeInfoTab.jsx +38 -40
- package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +0 -133
- package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +9 -7
- package/src/app/pages/NodePage/index.jsx +12 -11
- package/src/app/pages/QueryPlannerPage/PreAggDetailsPanel.jsx +46 -1
- package/src/app/pages/QueryPlannerPage/ResultsView.jsx +281 -0
- package/src/app/pages/QueryPlannerPage/SelectionPanel.jsx +225 -100
- package/src/app/pages/QueryPlannerPage/__tests__/MetricFlowGraph.test.jsx +193 -0
- package/src/app/pages/QueryPlannerPage/__tests__/ResultsView.test.jsx +388 -0
- package/src/app/pages/QueryPlannerPage/__tests__/SelectionPanel.test.jsx +31 -51
- package/src/app/pages/QueryPlannerPage/__tests__/index.test.jsx +720 -34
- package/src/app/pages/QueryPlannerPage/index.jsx +237 -117
- package/src/app/pages/QueryPlannerPage/styles.css +765 -15
- package/src/app/services/DJService.js +29 -6
- package/src/app/services/__tests__/DJService.test.jsx +163 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
2
|
+
import { ResultsView } from '../ResultsView';
|
|
3
|
+
|
|
4
|
+
// Mock react-syntax-highlighter
|
|
5
|
+
jest.mock('react-syntax-highlighter', () => {
|
|
6
|
+
const MockLight = ({ children }) => (
|
|
7
|
+
<pre data-testid="syntax-highlighter">{children}</pre>
|
|
8
|
+
);
|
|
9
|
+
MockLight.registerLanguage = jest.fn();
|
|
10
|
+
return { Light: MockLight };
|
|
11
|
+
});
|
|
12
|
+
jest.mock('react-syntax-highlighter/src/styles/hljs', () => ({
|
|
13
|
+
foundation: {},
|
|
14
|
+
}));
|
|
15
|
+
jest.mock('react-syntax-highlighter/dist/esm/languages/hljs/sql', () => ({}));
|
|
16
|
+
|
|
17
|
+
// Mock clipboard API
|
|
18
|
+
const mockWriteText = jest.fn();
|
|
19
|
+
Object.assign(navigator, {
|
|
20
|
+
clipboard: {
|
|
21
|
+
writeText: mockWriteText,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('ResultsView', () => {
|
|
26
|
+
const defaultProps = {
|
|
27
|
+
sql: 'SELECT * FROM table',
|
|
28
|
+
results: {
|
|
29
|
+
results: [
|
|
30
|
+
{
|
|
31
|
+
columns: [
|
|
32
|
+
{ name: 'id', type: 'INT' },
|
|
33
|
+
{ name: 'name', type: 'STRING' },
|
|
34
|
+
],
|
|
35
|
+
rows: [
|
|
36
|
+
[1, 'Alice'],
|
|
37
|
+
[2, 'Bob'],
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
loading: false,
|
|
43
|
+
error: null,
|
|
44
|
+
elapsedTime: 1.5,
|
|
45
|
+
onBackToPlan: jest.fn(),
|
|
46
|
+
selectedMetrics: ['metric1', 'metric2'],
|
|
47
|
+
selectedDimensions: ['dim1'],
|
|
48
|
+
filters: [],
|
|
49
|
+
dialect: 'SPARK',
|
|
50
|
+
cubeName: null,
|
|
51
|
+
availability: null,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
jest.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Header', () => {
|
|
59
|
+
it('renders back to plan button', () => {
|
|
60
|
+
render(<ResultsView {...defaultProps} />);
|
|
61
|
+
expect(screen.getByText('Back to Plan')).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('calls onBackToPlan when back button is clicked', () => {
|
|
65
|
+
const onBackToPlan = jest.fn();
|
|
66
|
+
render(<ResultsView {...defaultProps} onBackToPlan={onBackToPlan} />);
|
|
67
|
+
|
|
68
|
+
fireEvent.click(screen.getByText('Back to Plan'));
|
|
69
|
+
expect(onBackToPlan).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('shows row count and elapsed time', () => {
|
|
73
|
+
render(<ResultsView {...defaultProps} />);
|
|
74
|
+
// Row count appears in both header and table section
|
|
75
|
+
expect(screen.getAllByText('2 rows').length).toBeGreaterThanOrEqual(1);
|
|
76
|
+
expect(screen.getByText('1.50s')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('shows loading state', () => {
|
|
80
|
+
render(<ResultsView {...defaultProps} loading={true} />);
|
|
81
|
+
expect(screen.getByText('Running query...')).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('shows error state in header', () => {
|
|
85
|
+
render(<ResultsView {...defaultProps} error="Query timeout" />);
|
|
86
|
+
expect(screen.getByText('Query failed')).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('handles null elapsedTime', () => {
|
|
90
|
+
render(<ResultsView {...defaultProps} elapsedTime={null} />);
|
|
91
|
+
expect(screen.getAllByText('2 rows').length).toBeGreaterThanOrEqual(1);
|
|
92
|
+
// Should not show elapsed time when null
|
|
93
|
+
expect(screen.queryByText(/^\d+\.\d+s$/)).not.toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('SQL Pane', () => {
|
|
98
|
+
it('displays SQL query', () => {
|
|
99
|
+
render(<ResultsView {...defaultProps} />);
|
|
100
|
+
expect(screen.getByText('SELECT * FROM table')).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('shows SQL Query title', () => {
|
|
104
|
+
render(<ResultsView {...defaultProps} />);
|
|
105
|
+
expect(screen.getByText('SQL Query')).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('shows generating message when no SQL', () => {
|
|
109
|
+
render(<ResultsView {...defaultProps} sql={null} />);
|
|
110
|
+
expect(screen.getByText('Generating SQL...')).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('renders copy button', () => {
|
|
114
|
+
render(<ResultsView {...defaultProps} />);
|
|
115
|
+
expect(screen.getByText('Copy')).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('copies SQL to clipboard when copy button clicked', async () => {
|
|
119
|
+
render(<ResultsView {...defaultProps} />);
|
|
120
|
+
|
|
121
|
+
fireEvent.click(screen.getByText('Copy'));
|
|
122
|
+
|
|
123
|
+
expect(mockWriteText).toHaveBeenCalledWith('SELECT * FROM table');
|
|
124
|
+
expect(screen.getByText('✓ Copied')).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('disables copy button when no SQL', () => {
|
|
128
|
+
render(<ResultsView {...defaultProps} sql={null} />);
|
|
129
|
+
expect(screen.getByText('Copy')).toBeDisabled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('shows materialization info when cubeName is provided', () => {
|
|
133
|
+
render(
|
|
134
|
+
<ResultsView
|
|
135
|
+
{...defaultProps}
|
|
136
|
+
cubeName="test_cube"
|
|
137
|
+
availability={{
|
|
138
|
+
catalog: 'catalog',
|
|
139
|
+
schema_: 'schema',
|
|
140
|
+
table: 'table',
|
|
141
|
+
validThroughTs: 1705363200000, // Jan 16, 2024
|
|
142
|
+
}}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(screen.getByText(/Using materialized cube/)).toBeInTheDocument();
|
|
147
|
+
expect(screen.getByText(/Valid thru/)).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('shows materialization info without availability date', () => {
|
|
151
|
+
render(<ResultsView {...defaultProps} cubeName="test_cube" />);
|
|
152
|
+
|
|
153
|
+
expect(screen.getByText(/Using materialized cube/)).toBeInTheDocument();
|
|
154
|
+
expect(screen.queryByText(/Valid thru/)).not.toBeInTheDocument();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('Loading State', () => {
|
|
159
|
+
it('shows loading spinner and message', () => {
|
|
160
|
+
render(<ResultsView {...defaultProps} loading={true} />);
|
|
161
|
+
|
|
162
|
+
expect(screen.getByText('Executing query...')).toBeInTheDocument();
|
|
163
|
+
expect(
|
|
164
|
+
screen.getByText(/Querying 2 metric\(s\) with 1 dimension\(s\)/),
|
|
165
|
+
).toBeInTheDocument();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Error State', () => {
|
|
170
|
+
it('shows error message', () => {
|
|
171
|
+
render(<ResultsView {...defaultProps} error="Connection failed" />);
|
|
172
|
+
|
|
173
|
+
expect(screen.getByText('Query Failed')).toBeInTheDocument();
|
|
174
|
+
expect(screen.getByText('Connection failed')).toBeInTheDocument();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('shows back to plan button in error state', () => {
|
|
178
|
+
const onBackToPlan = jest.fn();
|
|
179
|
+
render(
|
|
180
|
+
<ResultsView
|
|
181
|
+
{...defaultProps}
|
|
182
|
+
error="Error"
|
|
183
|
+
onBackToPlan={onBackToPlan}
|
|
184
|
+
/>,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// There are two back buttons - one in header, one in error state
|
|
188
|
+
const backButtons = screen.getAllByText('Back to Plan');
|
|
189
|
+
expect(backButtons.length).toBe(2);
|
|
190
|
+
|
|
191
|
+
fireEvent.click(backButtons[1]);
|
|
192
|
+
expect(onBackToPlan).toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('Results Table', () => {
|
|
197
|
+
it('renders table with columns and rows', () => {
|
|
198
|
+
render(<ResultsView {...defaultProps} />);
|
|
199
|
+
|
|
200
|
+
expect(screen.getByText('id')).toBeInTheDocument();
|
|
201
|
+
expect(screen.getByText('name')).toBeInTheDocument();
|
|
202
|
+
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
203
|
+
expect(screen.getByText('Bob')).toBeInTheDocument();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('shows column types', () => {
|
|
207
|
+
render(<ResultsView {...defaultProps} />);
|
|
208
|
+
|
|
209
|
+
expect(screen.getByText('INT')).toBeInTheDocument();
|
|
210
|
+
expect(screen.getByText('STRING')).toBeInTheDocument();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('shows row count in table header', () => {
|
|
214
|
+
render(<ResultsView {...defaultProps} />);
|
|
215
|
+
|
|
216
|
+
// Row count appears in both header and table section
|
|
217
|
+
const rowCounts = screen.getAllByText('2 rows');
|
|
218
|
+
expect(rowCounts.length).toBeGreaterThanOrEqual(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('shows empty state when no results', () => {
|
|
222
|
+
render(
|
|
223
|
+
<ResultsView
|
|
224
|
+
{...defaultProps}
|
|
225
|
+
results={{ results: [{ columns: [], rows: [] }] }}
|
|
226
|
+
/>,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
expect(screen.getByText('No results returned')).toBeInTheDocument();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('handles null values in cells', () => {
|
|
233
|
+
render(
|
|
234
|
+
<ResultsView
|
|
235
|
+
{...defaultProps}
|
|
236
|
+
results={{
|
|
237
|
+
results: [
|
|
238
|
+
{
|
|
239
|
+
columns: [{ name: 'value', type: 'STRING' }],
|
|
240
|
+
rows: [[null], ['data']],
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
}}
|
|
244
|
+
/>,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(screen.getByText('NULL')).toBeInTheDocument();
|
|
248
|
+
expect(screen.getByText('data')).toBeInTheDocument();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('displays filters as chips', () => {
|
|
252
|
+
render(
|
|
253
|
+
<ResultsView
|
|
254
|
+
{...defaultProps}
|
|
255
|
+
filters={["date >= '2024-01-01'", "status = 'active'"]}
|
|
256
|
+
/>,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
expect(screen.getByText("date >= '2024-01-01'")).toBeInTheDocument();
|
|
260
|
+
expect(screen.getByText("status = 'active'")).toBeInTheDocument();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('Sorting', () => {
|
|
265
|
+
it('sorts by column when header is clicked', () => {
|
|
266
|
+
render(<ResultsView {...defaultProps} />);
|
|
267
|
+
|
|
268
|
+
// Click on 'name' column header to sort
|
|
269
|
+
const nameHeader = screen.getByText('name').closest('th');
|
|
270
|
+
fireEvent.click(nameHeader);
|
|
271
|
+
|
|
272
|
+
// First row should now be Alice (ascending)
|
|
273
|
+
const rows = screen.getAllByRole('row');
|
|
274
|
+
expect(rows[1]).toHaveTextContent('Alice');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('toggles sort direction on second click', () => {
|
|
278
|
+
render(<ResultsView {...defaultProps} />);
|
|
279
|
+
|
|
280
|
+
const nameHeader = screen.getByText('name').closest('th');
|
|
281
|
+
|
|
282
|
+
// First click - ascending
|
|
283
|
+
fireEvent.click(nameHeader);
|
|
284
|
+
let rows = screen.getAllByRole('row');
|
|
285
|
+
expect(rows[1]).toHaveTextContent('Alice');
|
|
286
|
+
|
|
287
|
+
// Second click - descending
|
|
288
|
+
fireEvent.click(nameHeader);
|
|
289
|
+
rows = screen.getAllByRole('row');
|
|
290
|
+
expect(rows[1]).toHaveTextContent('Bob');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('shows active sort indicator', () => {
|
|
294
|
+
render(<ResultsView {...defaultProps} />);
|
|
295
|
+
|
|
296
|
+
const nameHeader = screen.getByText('name').closest('th');
|
|
297
|
+
fireEvent.click(nameHeader);
|
|
298
|
+
|
|
299
|
+
expect(nameHeader).toHaveClass('sorted');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('sorts numeric columns correctly', () => {
|
|
303
|
+
render(
|
|
304
|
+
<ResultsView
|
|
305
|
+
{...defaultProps}
|
|
306
|
+
results={{
|
|
307
|
+
results: [
|
|
308
|
+
{
|
|
309
|
+
columns: [{ name: 'count', type: 'INT' }],
|
|
310
|
+
rows: [[10], [2], [100]],
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
}}
|
|
314
|
+
/>,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const countHeader = screen.getByText('count').closest('th');
|
|
318
|
+
fireEvent.click(countHeader);
|
|
319
|
+
|
|
320
|
+
const cells = screen.getAllByRole('cell');
|
|
321
|
+
expect(cells[0]).toHaveTextContent('2');
|
|
322
|
+
expect(cells[1]).toHaveTextContent('10');
|
|
323
|
+
expect(cells[2]).toHaveTextContent('100');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('handles null values in sorting - nulls go last', () => {
|
|
327
|
+
render(
|
|
328
|
+
<ResultsView
|
|
329
|
+
{...defaultProps}
|
|
330
|
+
results={{
|
|
331
|
+
results: [
|
|
332
|
+
{
|
|
333
|
+
columns: [{ name: 'value', type: 'STRING' }],
|
|
334
|
+
rows: [[null], ['b'], ['a']],
|
|
335
|
+
},
|
|
336
|
+
],
|
|
337
|
+
}}
|
|
338
|
+
/>,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const valueHeader = screen.getByText('value').closest('th');
|
|
342
|
+
fireEvent.click(valueHeader);
|
|
343
|
+
|
|
344
|
+
const cells = screen.getAllByRole('cell');
|
|
345
|
+
expect(cells[0]).toHaveTextContent('a');
|
|
346
|
+
expect(cells[1]).toHaveTextContent('b');
|
|
347
|
+
expect(cells[2]).toHaveTextContent('NULL');
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe('Edge Cases', () => {
|
|
352
|
+
it('handles missing results gracefully', () => {
|
|
353
|
+
render(<ResultsView {...defaultProps} results={null} />);
|
|
354
|
+
|
|
355
|
+
// Row count appears in both header and table section
|
|
356
|
+
expect(screen.getAllByText('0 rows').length).toBeGreaterThanOrEqual(1);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('handles empty results object', () => {
|
|
360
|
+
render(<ResultsView {...defaultProps} results={{}} />);
|
|
361
|
+
|
|
362
|
+
// Row count appears in both header and table section
|
|
363
|
+
expect(screen.getAllByText('0 rows').length).toBeGreaterThanOrEqual(1);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('formats large row counts with locale', () => {
|
|
367
|
+
const manyRows = Array.from({ length: 1000 }, (_, i) => [i]);
|
|
368
|
+
render(
|
|
369
|
+
<ResultsView
|
|
370
|
+
{...defaultProps}
|
|
371
|
+
results={{
|
|
372
|
+
results: [
|
|
373
|
+
{
|
|
374
|
+
columns: [{ name: 'id', type: 'INT' }],
|
|
375
|
+
rows: manyRows,
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
}}
|
|
379
|
+
/>,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Should show "1,000 rows" with locale formatting (appears in both header and table)
|
|
383
|
+
expect(screen.getAllByText('1,000 rows').length).toBeGreaterThanOrEqual(
|
|
384
|
+
1,
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
});
|
|
@@ -199,18 +199,6 @@ describe('SelectionPanel', () => {
|
|
|
199
199
|
const items = screen.getAllByRole('checkbox');
|
|
200
200
|
expect(items.length).toBeGreaterThan(0);
|
|
201
201
|
});
|
|
202
|
-
|
|
203
|
-
it('clears search when clear button is clicked', () => {
|
|
204
|
-
render(<SelectionPanel {...defaultProps} />);
|
|
205
|
-
|
|
206
|
-
const searchInput = screen.getByPlaceholderText('Search metrics...');
|
|
207
|
-
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
208
|
-
|
|
209
|
-
const clearButton = screen.getAllByText('×')[0];
|
|
210
|
-
fireEvent.click(clearButton);
|
|
211
|
-
|
|
212
|
-
expect(searchInput.value).toBe('');
|
|
213
|
-
});
|
|
214
202
|
});
|
|
215
203
|
|
|
216
204
|
describe('Select All / Clear Actions', () => {
|
|
@@ -220,7 +208,13 @@ describe('SelectionPanel', () => {
|
|
|
220
208
|
fireEvent.click(screen.getByText('default'));
|
|
221
209
|
|
|
222
210
|
expect(screen.getByText('Select all')).toBeInTheDocument();
|
|
223
|
-
|
|
211
|
+
// Check for namespace-level Clear button (inside namespace-actions)
|
|
212
|
+
// Both buttons have select-all-btn class, Clear is the second one
|
|
213
|
+
const namespaceButtons = document.querySelectorAll(
|
|
214
|
+
'.namespace-actions .select-all-btn',
|
|
215
|
+
);
|
|
216
|
+
expect(namespaceButtons.length).toBe(2);
|
|
217
|
+
expect(namespaceButtons[1].textContent).toBe('Clear');
|
|
224
218
|
});
|
|
225
219
|
|
|
226
220
|
it('selects all metrics in namespace when Select all is clicked', () => {
|
|
@@ -239,7 +233,7 @@ describe('SelectionPanel', () => {
|
|
|
239
233
|
]);
|
|
240
234
|
});
|
|
241
235
|
|
|
242
|
-
it('clears all metrics in namespace when Clear is clicked', () => {
|
|
236
|
+
it('clears all metrics in namespace when namespace Clear is clicked', () => {
|
|
243
237
|
const onMetricsChange = jest.fn();
|
|
244
238
|
render(
|
|
245
239
|
<SelectionPanel
|
|
@@ -253,7 +247,11 @@ describe('SelectionPanel', () => {
|
|
|
253
247
|
);
|
|
254
248
|
|
|
255
249
|
fireEvent.click(screen.getByText('default'));
|
|
256
|
-
|
|
250
|
+
// Click the namespace-level Clear button (second button in namespace-actions)
|
|
251
|
+
const namespaceButtons = document.querySelectorAll(
|
|
252
|
+
'.namespace-actions .select-all-btn',
|
|
253
|
+
);
|
|
254
|
+
fireEvent.click(namespaceButtons[1]); // Clear is the second button
|
|
257
255
|
|
|
258
256
|
expect(onMetricsChange).toHaveBeenCalledWith([]);
|
|
259
257
|
});
|
|
@@ -571,7 +569,7 @@ describe('SelectionPanel', () => {
|
|
|
571
569
|
/>,
|
|
572
570
|
);
|
|
573
571
|
|
|
574
|
-
expect(screen.getByText(
|
|
572
|
+
expect(screen.getByText('Show all')).toBeInTheDocument();
|
|
575
573
|
});
|
|
576
574
|
|
|
577
575
|
it('toggles chips expansion when Show all/Show less is clicked', () => {
|
|
@@ -588,7 +586,7 @@ describe('SelectionPanel', () => {
|
|
|
588
586
|
);
|
|
589
587
|
|
|
590
588
|
// Click to expand
|
|
591
|
-
const expandBtn = screen.getByText(
|
|
589
|
+
const expandBtn = screen.getByText('Show all');
|
|
592
590
|
fireEvent.click(expandBtn);
|
|
593
591
|
|
|
594
592
|
// Should now show "Show less"
|
|
@@ -598,7 +596,7 @@ describe('SelectionPanel', () => {
|
|
|
598
596
|
fireEvent.click(screen.getByText('Show less'));
|
|
599
597
|
|
|
600
598
|
// Should show "Show all" again
|
|
601
|
-
expect(screen.getByText(
|
|
599
|
+
expect(screen.getByText('Show all')).toBeInTheDocument();
|
|
602
600
|
});
|
|
603
601
|
});
|
|
604
602
|
|
|
@@ -616,10 +614,8 @@ describe('SelectionPanel', () => {
|
|
|
616
614
|
const chipElements = screen.getAllByText('date_dim.dateint');
|
|
617
615
|
// Should have at least one chip (and possibly one in the list)
|
|
618
616
|
expect(chipElements.length).toBeGreaterThanOrEqual(1);
|
|
619
|
-
// The chip should have the chip
|
|
620
|
-
expect(
|
|
621
|
-
document.querySelector('.dimension-chip .chip-label'),
|
|
622
|
-
).toBeInTheDocument();
|
|
617
|
+
// The chip should have the dimension-chip class
|
|
618
|
+
expect(document.querySelector('.dimension-chip')).toBeInTheDocument();
|
|
623
619
|
});
|
|
624
620
|
|
|
625
621
|
it('removes dimension when chip remove button is clicked', () => {
|
|
@@ -646,8 +642,8 @@ describe('SelectionPanel', () => {
|
|
|
646
642
|
});
|
|
647
643
|
});
|
|
648
644
|
|
|
649
|
-
describe('Clear
|
|
650
|
-
it('shows Clear
|
|
645
|
+
describe('Clear Button', () => {
|
|
646
|
+
it('shows global Clear button when items are selected', () => {
|
|
651
647
|
render(
|
|
652
648
|
<SelectionPanel
|
|
653
649
|
{...defaultProps}
|
|
@@ -656,10 +652,12 @@ describe('SelectionPanel', () => {
|
|
|
656
652
|
/>,
|
|
657
653
|
);
|
|
658
654
|
|
|
659
|
-
|
|
655
|
+
const clearAllBtn = document.querySelector('.clear-all-btn');
|
|
656
|
+
expect(clearAllBtn).toBeInTheDocument();
|
|
657
|
+
expect(clearAllBtn.textContent).toBe('Clear');
|
|
660
658
|
});
|
|
661
659
|
|
|
662
|
-
it('calls onClearSelection when Clear
|
|
660
|
+
it('calls onClearSelection when global Clear is clicked', () => {
|
|
663
661
|
const onClearSelection = jest.fn();
|
|
664
662
|
render(
|
|
665
663
|
<SelectionPanel
|
|
@@ -670,7 +668,8 @@ describe('SelectionPanel', () => {
|
|
|
670
668
|
/>,
|
|
671
669
|
);
|
|
672
670
|
|
|
673
|
-
|
|
671
|
+
const clearAllBtn = document.querySelector('.clear-all-btn');
|
|
672
|
+
fireEvent.click(clearAllBtn);
|
|
674
673
|
|
|
675
674
|
expect(onClearSelection).toHaveBeenCalled();
|
|
676
675
|
});
|
|
@@ -689,7 +688,8 @@ describe('SelectionPanel', () => {
|
|
|
689
688
|
/>,
|
|
690
689
|
);
|
|
691
690
|
|
|
692
|
-
|
|
691
|
+
const clearAllBtn = document.querySelector('.clear-all-btn');
|
|
692
|
+
fireEvent.click(clearAllBtn);
|
|
693
693
|
|
|
694
694
|
expect(onMetricsChange).toHaveBeenCalledWith([]);
|
|
695
695
|
expect(onDimensionsChange).toHaveBeenCalledWith([]);
|
|
@@ -865,7 +865,7 @@ describe('SelectionPanel', () => {
|
|
|
865
865
|
/>,
|
|
866
866
|
);
|
|
867
867
|
|
|
868
|
-
expect(screen.getByText(
|
|
868
|
+
expect(screen.getByText('Show all')).toBeInTheDocument();
|
|
869
869
|
});
|
|
870
870
|
|
|
871
871
|
it('toggles dimension chips expansion', () => {
|
|
@@ -884,7 +884,7 @@ describe('SelectionPanel', () => {
|
|
|
884
884
|
);
|
|
885
885
|
|
|
886
886
|
// Click to expand
|
|
887
|
-
const expandBtn = screen.getByText(
|
|
887
|
+
const expandBtn = screen.getByText('Show all');
|
|
888
888
|
fireEvent.click(expandBtn);
|
|
889
889
|
|
|
890
890
|
// Should show "Show less"
|
|
@@ -894,7 +894,7 @@ describe('SelectionPanel', () => {
|
|
|
894
894
|
fireEvent.click(screen.getByText('Show less'));
|
|
895
895
|
|
|
896
896
|
// Should show "Show all" again
|
|
897
|
-
expect(screen.getByText(
|
|
897
|
+
expect(screen.getByText('Show all')).toBeInTheDocument();
|
|
898
898
|
});
|
|
899
899
|
});
|
|
900
900
|
|
|
@@ -928,26 +928,6 @@ describe('SelectionPanel', () => {
|
|
|
928
928
|
});
|
|
929
929
|
});
|
|
930
930
|
|
|
931
|
-
describe('Clear Dimension Search', () => {
|
|
932
|
-
it('clears dimension search when clear button is clicked', () => {
|
|
933
|
-
render(
|
|
934
|
-
<SelectionPanel {...defaultProps} selectedMetrics={['default.test']} />,
|
|
935
|
-
);
|
|
936
|
-
|
|
937
|
-
const searchInput = screen.getByPlaceholderText('Search dimensions...');
|
|
938
|
-
fireEvent.change(searchInput, { target: { value: 'test' } });
|
|
939
|
-
|
|
940
|
-
expect(searchInput.value).toBe('test');
|
|
941
|
-
|
|
942
|
-
// Find the clear button (there are two × buttons, one for each search)
|
|
943
|
-
const clearButtons = screen.getAllByText('×');
|
|
944
|
-
// The second one is for dimension search
|
|
945
|
-
fireEvent.click(clearButtons[clearButtons.length - 1]);
|
|
946
|
-
|
|
947
|
-
expect(searchInput.value).toBe('');
|
|
948
|
-
});
|
|
949
|
-
});
|
|
950
|
-
|
|
951
931
|
describe('Remove Dimension from Selected', () => {
|
|
952
932
|
it('removes dimension when clicking X on dimension chip', () => {
|
|
953
933
|
const onDimensionsChange = jest.fn();
|