mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-18 07:56:56 +00:00
feat: added new component to existing variables (#7744)
* feat: added new component to existing variables * feat: added multiselect component and solved dropdown closing on every selection * feat: fixed incorrect all label * feat: allow custom value * feat: better styles * feat: added maxtagcount placeholder * feat: added onclear function * feat: updated regex and handlings * feat: updated regex and handlings * feat: added enableall prop control * feat: fix the rebase conflict * feat: fixed comments * feat: fix test case * feat: added test cases for customMultiselect, customSelect and variableItem integration * feat: added test cases for behaviour around values added and selections showed * feat: refactored test cases --------- Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
parent
bf704333b3
commit
1aa7e8b5d9
@ -37,7 +37,7 @@ enum ToggleTagValue {
|
|||||||
All = 'All',
|
All = 'All',
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALL_SELECTED_VALUE = '__all__'; // Constant for the special value
|
const ALL_SELECTED_VALUE = '__ALL__'; // Constant for the special value
|
||||||
|
|
||||||
const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||||
placeholder = 'Search...',
|
placeholder = 'Search...',
|
||||||
@ -62,6 +62,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
allowClear = false,
|
allowClear = false,
|
||||||
onRetry,
|
onRetry,
|
||||||
maxTagTextLength,
|
maxTagTextLength,
|
||||||
|
onDropdownVisibleChange,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
// ===== State & Refs =====
|
// ===== State & Refs =====
|
||||||
@ -144,7 +145,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 2: "__all__" is selected (means select all actual values)
|
// Case 2: "__ALL__" is selected (means select all actual values)
|
||||||
if (currentNewValue.includes(ALL_SELECTED_VALUE)) {
|
if (currentNewValue.includes(ALL_SELECTED_VALUE)) {
|
||||||
const allActualOptions = allAvailableValues.map(
|
const allActualOptions = allAvailableValues.map(
|
||||||
(v) => options.flat().find((o) => o.value === v) || { label: v, value: v },
|
(v) => options.flat().find((o) => o.value === v) || { label: v, value: v },
|
||||||
@ -272,7 +273,8 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
: filteredOptions,
|
: filteredOptions,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [filteredOptions, searchText, options, selectedValues]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [filteredOptions, searchText, options]);
|
||||||
|
|
||||||
// ===== Text Selection Utilities =====
|
// ===== Text Selection Utilities =====
|
||||||
|
|
||||||
@ -528,28 +530,33 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
(text: string, searchQuery: string): React.ReactNode => {
|
(text: string, searchQuery: string): React.ReactNode => {
|
||||||
if (!searchQuery || !highlightSearch) return text;
|
if (!searchQuery || !highlightSearch) return text;
|
||||||
|
|
||||||
const parts = text.split(
|
try {
|
||||||
new RegExp(
|
const parts = text.split(
|
||||||
`(${searchQuery.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')})`,
|
new RegExp(
|
||||||
'gi',
|
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
|
||||||
),
|
'gi',
|
||||||
);
|
),
|
||||||
return (
|
);
|
||||||
<>
|
return (
|
||||||
{parts.map((part, i) => {
|
<>
|
||||||
// Create a unique key that doesn't rely on array index
|
{parts.map((part, i) => {
|
||||||
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
// Create a unique key that doesn't rely on array index
|
||||||
|
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
||||||
|
|
||||||
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
return part.trim().toLowerCase() === searchQuery.trim().toLowerCase() ? (
|
||||||
<span key={uniqueKey} className="highlight-text">
|
<span key={uniqueKey} className="highlight-text">
|
||||||
{part}
|
{part}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
part
|
part
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// If regex fails, return the original text without highlighting
|
||||||
|
return text;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[highlightSearch],
|
[highlightSearch],
|
||||||
);
|
);
|
||||||
@ -752,7 +759,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
if (hasAll) {
|
if (hasAll) {
|
||||||
flatList.push({
|
flatList.push({
|
||||||
label: 'ALL',
|
label: 'ALL',
|
||||||
value: '__all__', // Special value for the ALL option
|
value: ALL_SELECTED_VALUE, // Special value for the ALL option
|
||||||
type: 'defined',
|
type: 'defined',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1129,7 +1136,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
// If there's an active option in the dropdown, prioritize selecting it
|
// If there's an active option in the dropdown, prioritize selecting it
|
||||||
if (activeIndex >= 0 && activeIndex < flatOptions.length) {
|
if (activeIndex >= 0 && activeIndex < flatOptions.length) {
|
||||||
const selectedOption = flatOptions[activeIndex];
|
const selectedOption = flatOptions[activeIndex];
|
||||||
if (selectedOption.value === '__all__') {
|
if (selectedOption.value === ALL_SELECTED_VALUE) {
|
||||||
handleSelectAll();
|
handleSelectAll();
|
||||||
} else if (selectedOption.value && onChange) {
|
} else if (selectedOption.value && onChange) {
|
||||||
const newValues = selectedValues.includes(selectedOption.value)
|
const newValues = selectedValues.includes(selectedOption.value)
|
||||||
@ -1159,6 +1166,10 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setActiveIndex(-1);
|
setActiveIndex(-1);
|
||||||
|
// Call onDropdownVisibleChange when Escape is pressed to close dropdown
|
||||||
|
if (onDropdownVisibleChange) {
|
||||||
|
onDropdownVisibleChange(false);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SPACEKEY:
|
case SPACEKEY:
|
||||||
@ -1168,7 +1179,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
const selectedOption = flatOptions[activeIndex];
|
const selectedOption = flatOptions[activeIndex];
|
||||||
|
|
||||||
// Check if it's the ALL option
|
// Check if it's the ALL option
|
||||||
if (selectedOption.value === '__all__') {
|
if (selectedOption.value === ALL_SELECTED_VALUE) {
|
||||||
handleSelectAll();
|
handleSelectAll();
|
||||||
} else if (selectedOption.value && onChange) {
|
} else if (selectedOption.value && onChange) {
|
||||||
const newValues = selectedValues.includes(selectedOption.value)
|
const newValues = selectedValues.includes(selectedOption.value)
|
||||||
@ -1282,6 +1293,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
handleSelectAll,
|
handleSelectAll,
|
||||||
getVisibleChipIndices,
|
getVisibleChipIndices,
|
||||||
getLastVisibleChipIndex,
|
getLastVisibleChipIndex,
|
||||||
|
onDropdownVisibleChange,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1524,6 +1536,18 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
onRetry,
|
onRetry,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Custom handler for dropdown visibility changes
|
||||||
|
const handleDropdownVisibleChange = useCallback(
|
||||||
|
(visible: boolean): void => {
|
||||||
|
setIsOpen(visible);
|
||||||
|
// Pass through to the parent component's handler if provided
|
||||||
|
if (onDropdownVisibleChange) {
|
||||||
|
onDropdownVisibleChange(visible);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onDropdownVisibleChange],
|
||||||
|
);
|
||||||
|
|
||||||
// ===== Side Effects =====
|
// ===== Side Effects =====
|
||||||
|
|
||||||
// Clear search when dropdown closes
|
// Clear search when dropdown closes
|
||||||
@ -1739,7 +1763,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
|||||||
value={displayValue}
|
value={displayValue}
|
||||||
onChange={handleInternalChange}
|
onChange={handleInternalChange}
|
||||||
onClear={(): void => handleInternalChange([])}
|
onClear={(): void => handleInternalChange([])}
|
||||||
onDropdownVisibleChange={setIsOpen}
|
onDropdownVisibleChange={handleDropdownVisibleChange}
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
defaultActiveFirstOption={defaultActiveFirstOption}
|
defaultActiveFirstOption={defaultActiveFirstOption}
|
||||||
popupMatchSelectWidth={dropdownMatchSelectWidth}
|
popupMatchSelectWidth={dropdownMatchSelectWidth}
|
||||||
|
|||||||
@ -130,23 +130,33 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
|
|||||||
(text: string, searchQuery: string): React.ReactNode => {
|
(text: string, searchQuery: string): React.ReactNode => {
|
||||||
if (!searchQuery || !highlightSearch) return text;
|
if (!searchQuery || !highlightSearch) return text;
|
||||||
|
|
||||||
const parts = text.split(new RegExp(`(${searchQuery})`, 'gi'));
|
try {
|
||||||
return (
|
const parts = text.split(
|
||||||
<>
|
new RegExp(
|
||||||
{parts.map((part, i) => {
|
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
|
||||||
// Create a deterministic but unique key
|
'gi',
|
||||||
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
),
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{parts.map((part, i) => {
|
||||||
|
// Create a deterministic but unique key
|
||||||
|
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
|
||||||
|
|
||||||
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
return part.toLowerCase() === searchQuery.toLowerCase() ? (
|
||||||
<span key={uniqueKey} className="highlight-text">
|
<span key={uniqueKey} className="highlight-text">
|
||||||
{part}
|
{part}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
part
|
part
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in text highlighting:', error);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[highlightSearch],
|
[highlightSearch],
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,820 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
import VariableItem from '../../../container/NewDashboard/DashboardVariablesSelection/VariableItem';
|
||||||
|
|
||||||
|
// Mock the dashboard variables query
|
||||||
|
jest.mock('api/dashboard/variables/dashboardVariablesQuery', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
payload: {
|
||||||
|
variableValues: ['option1', 'option2', 'option3', 'option4'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock scrollIntoView which isn't available in JSDOM
|
||||||
|
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const TEST_VARIABLE_NAME = 'test_variable';
|
||||||
|
const TEST_VARIABLE_ID = 'test-var-id';
|
||||||
|
|
||||||
|
// Create a mock store
|
||||||
|
const mockStore = configureStore([])({
|
||||||
|
globalTime: {
|
||||||
|
minTime: Date.now() - 3600000, // 1 hour ago
|
||||||
|
maxTime: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test data
|
||||||
|
const createMockVariable = (
|
||||||
|
overrides: Partial<IDashboardVariable> = {},
|
||||||
|
): IDashboardVariable => ({
|
||||||
|
id: TEST_VARIABLE_ID,
|
||||||
|
name: TEST_VARIABLE_NAME,
|
||||||
|
description: 'Test variable description',
|
||||||
|
type: 'QUERY',
|
||||||
|
queryValue: 'SELECT DISTINCT value FROM table',
|
||||||
|
customValue: '',
|
||||||
|
sort: 'ASC',
|
||||||
|
multiSelect: false,
|
||||||
|
showALLOption: true,
|
||||||
|
selectedValue: [],
|
||||||
|
allSelected: false,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
function TestWrapper({ children }: { children: React.ReactNode }): JSX.Element {
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: { retry: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider store={mockStore}>
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('VariableItem Integration Tests', () => {
|
||||||
|
let user: ReturnType<typeof userEvent.setup>;
|
||||||
|
let mockOnValueUpdate: jest.Mock;
|
||||||
|
let mockSetVariablesToGetUpdated: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = userEvent.setup();
|
||||||
|
mockOnValueUpdate = jest.fn();
|
||||||
|
mockSetVariablesToGetUpdated = jest.fn();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 1. INTEGRATION WITH CUSTOMSELECT =====
|
||||||
|
describe('CustomSelect Integration (VI)', () => {
|
||||||
|
test('VI-01: Single select variable integration', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
multiSelect: false,
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2,option3',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should render with CustomSelect
|
||||||
|
const combobox = screen.getByRole('combobox');
|
||||||
|
expect(combobox).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(combobox);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('option1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('option2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('option3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select an option
|
||||||
|
const option1 = screen.getByText('option1');
|
||||||
|
await user.click(option1);
|
||||||
|
|
||||||
|
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
||||||
|
TEST_VARIABLE_NAME,
|
||||||
|
TEST_VARIABLE_ID,
|
||||||
|
'option1',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 2. INTEGRATION WITH CUSTOMMULTISELECT =====
|
||||||
|
describe('CustomMultiSelect Integration (VI)', () => {
|
||||||
|
test('VI-02: Multi select variable integration', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
multiSelect: true,
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2,option3,option4',
|
||||||
|
showALLOption: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should render with CustomMultiSelect
|
||||||
|
const combobox = screen.getByRole('combobox');
|
||||||
|
expect(combobox).toBeInTheDocument();
|
||||||
|
|
||||||
|
await user.click(combobox);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Should show ALL option
|
||||||
|
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('option1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('option2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 3. TEXTBOX VARIABLE TYPE =====
|
||||||
|
describe('Textbox Variable Integration', () => {
|
||||||
|
test('VI-03: Textbox variable handling', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
type: 'TEXTBOX',
|
||||||
|
selectedValue: 'initial-value',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should render a regular input
|
||||||
|
const textInput = screen.getByDisplayValue('initial-value');
|
||||||
|
expect(textInput).toBeInTheDocument();
|
||||||
|
expect(textInput.tagName).toBe('INPUT');
|
||||||
|
|
||||||
|
// Clear and type new value
|
||||||
|
await user.clear(textInput);
|
||||||
|
await user.type(textInput, 'new-text-value');
|
||||||
|
|
||||||
|
// Should call onValueUpdate after debounce
|
||||||
|
await waitFor(
|
||||||
|
() => {
|
||||||
|
expect(mockOnValueUpdate).toHaveBeenCalledWith(
|
||||||
|
TEST_VARIABLE_NAME,
|
||||||
|
TEST_VARIABLE_ID,
|
||||||
|
'new-text-value',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ timeout: 1000 },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 4. VALUE PERSISTENCE AND STATE MANAGEMENT =====
|
||||||
|
describe('Value Persistence and State Management', () => {
|
||||||
|
test('VI-04: All selected state handling', () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
multiSelect: true,
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'service1,service2,service3',
|
||||||
|
selectedValue: ['service1', 'service2', 'service3'],
|
||||||
|
allSelected: true,
|
||||||
|
showALLOption: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show "ALL" instead of individual values
|
||||||
|
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('VI-05: Dropdown behavior with temporary selections', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
multiSelect: true,
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'item1,item2,item3',
|
||||||
|
selectedValue: ['item1'],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const combobox = screen.getByRole('combobox');
|
||||||
|
await user.click(combobox);
|
||||||
|
|
||||||
|
// Select additional items
|
||||||
|
await waitFor(() => {
|
||||||
|
const item2 = screen.getByText('item2');
|
||||||
|
expect(item2).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const item2 = screen.getByText('item2');
|
||||||
|
await user.click(item2);
|
||||||
|
|
||||||
|
// Should not immediately close dropdown for multiselect
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('item3')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 6. ACCESSIBILITY AND USER EXPERIENCE =====
|
||||||
|
describe('Accessibility and User Experience', () => {
|
||||||
|
test('VI-06: Variable description tooltip', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
description: 'This variable controls the service selection',
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'service1,service2',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show info icon
|
||||||
|
const infoIcon = document.querySelector('.info-icon');
|
||||||
|
expect(infoIcon).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Hover to show tooltip
|
||||||
|
if (infoIcon) {
|
||||||
|
await user.hover(infoIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
screen.getByText('This variable controls the service selection'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('VI-07: Variable name display', () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
name: 'service_name',
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'service1,service2',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show variable name with $ prefix
|
||||||
|
expect(screen.getByText('$service_name')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('VI-08: Max tag count behavior', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
multiSelect: true,
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'tag1,tag2,tag3,tag4,tag5,tag6,tag7',
|
||||||
|
selectedValue: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5', 'tag6', 'tag7'],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show limited number of tags with "+ X more"
|
||||||
|
const tags = document.querySelectorAll('.ant-select-selection-item');
|
||||||
|
|
||||||
|
// Should show some tags and potentially a "+X more" indicator
|
||||||
|
expect(tags.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check for overflow indicator (maxTagCount is set to 4 in the component)
|
||||||
|
// With 7 tags and maxTagCount of 4, should show "+ 3 more"
|
||||||
|
if (tags.length > 4) {
|
||||||
|
const overflowIndicator = document.querySelector('[title*=","]');
|
||||||
|
expect(overflowIndicator).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Verify the "+ N more" text is displayed
|
||||||
|
const overflowText = overflowIndicator?.textContent;
|
||||||
|
expect(overflowText).toMatch(/\+ \d+ more/);
|
||||||
|
|
||||||
|
// Should show exactly "+ 3 more" for 7 tags with maxTagCount of 4
|
||||||
|
expect(overflowText).toBe('+ 3 more');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 8. SEARCH INTERACTION TESTS =====
|
||||||
|
describe('Search Interaction Tests', () => {
|
||||||
|
test('VI-12: Search filtering in multiselect', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'frontend,backend,database,api',
|
||||||
|
multiSelect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const combobox = screen.getByRole('combobox');
|
||||||
|
await user.click(combobox);
|
||||||
|
|
||||||
|
// Wait for dropdown to open
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find and type in search input
|
||||||
|
const searchInput = document.querySelector(
|
||||||
|
'.ant-select-selection-search-input',
|
||||||
|
);
|
||||||
|
expect(searchInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
if (searchInput) {
|
||||||
|
await user.type(searchInput, 'front');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should filter to show only options containing 'front'
|
||||||
|
await waitFor(() => {
|
||||||
|
// Check for highlighted text within the Frontend option
|
||||||
|
const highlightedElements = document.querySelectorAll('.highlight-text');
|
||||||
|
const highlightTexts = Array.from(highlightedElements).map(
|
||||||
|
(el) => el.textContent,
|
||||||
|
);
|
||||||
|
expect(highlightTexts).toContain('front');
|
||||||
|
|
||||||
|
// Should show Frontend option (highlighted) - use a simpler approach
|
||||||
|
const optionContents = document.querySelectorAll('.option-content');
|
||||||
|
const hasFrontendOption = Array.from(optionContents).some((content) =>
|
||||||
|
content.textContent?.includes('frontend'),
|
||||||
|
);
|
||||||
|
expect(hasFrontendOption).toBe(true);
|
||||||
|
|
||||||
|
// Backend and Database should not be visible
|
||||||
|
expect(screen.queryByText('backend')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('database')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('VI-13: Custom value creation workflow', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2,option3',
|
||||||
|
multiSelect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const combobox = screen.getByRole('combobox');
|
||||||
|
await user.click(combobox);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = document.querySelector(
|
||||||
|
'.ant-select-selection-search-input',
|
||||||
|
);
|
||||||
|
expect(searchInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
if (searchInput) {
|
||||||
|
await user.type(searchInput, 'custom-value');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that custom value appears in dropdown with custom tag
|
||||||
|
await waitFor(() => {
|
||||||
|
// Find the custom option with "custom-value" text and "Custom" badge
|
||||||
|
const customOptions = screen.getAllByText('custom-value');
|
||||||
|
const customOption = customOptions.find((option) => {
|
||||||
|
const optionItem = option.closest('.option-item');
|
||||||
|
const badge = optionItem?.querySelector('.option-badge');
|
||||||
|
return badge?.textContent === 'Custom';
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(customOption).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Verify it has the custom badge
|
||||||
|
const optionItem = customOption?.closest('.option-item');
|
||||||
|
const badge = optionItem?.querySelector('.option-badge');
|
||||||
|
expect(badge).toBeInTheDocument();
|
||||||
|
expect(badge?.textContent).toBe('Custom');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Press Enter to create the custom value
|
||||||
|
await user.keyboard('{Enter}');
|
||||||
|
|
||||||
|
// Should create a custom value and call onValueUpdate
|
||||||
|
await waitFor(() => {
|
||||||
|
// The custom value was created (we can see it in the DOM)
|
||||||
|
// but the callback might not be called immediately
|
||||||
|
// Let's check if the custom value is in the selection
|
||||||
|
const selectionItems = document.querySelectorAll(
|
||||||
|
'.ant-select-selection-item',
|
||||||
|
);
|
||||||
|
const hasCustomValue = Array.from(selectionItems).some((item) =>
|
||||||
|
item.textContent?.includes('custom-value'),
|
||||||
|
);
|
||||||
|
expect(hasCustomValue).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('VI-14: Search persistence across dropdown open/close', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2,option3',
|
||||||
|
multiSelect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const combobox = screen.getByRole('combobox');
|
||||||
|
await user.click(combobox);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = document.querySelector(
|
||||||
|
'.ant-select-selection-search-input',
|
||||||
|
);
|
||||||
|
expect(searchInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
if (searchInput) {
|
||||||
|
await user.type(searchInput, 'search-text');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify search text is in input
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(searchInput).toHaveValue('search-text');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Press Escape to close dropdown
|
||||||
|
await user.keyboard('{Escape}');
|
||||||
|
|
||||||
|
// Dropdown should close and search text should be cleared
|
||||||
|
await waitFor(() => {
|
||||||
|
const dropdown = document.querySelector('.ant-select-dropdown');
|
||||||
|
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
|
||||||
|
expect(searchInput).toHaveValue('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 9. ADVANCED KEYBOARD NAVIGATION =====
|
||||||
|
describe('Advanced Keyboard Navigation (VI)', () => {
|
||||||
|
test('VI-15: Shift + Arrow + Del chip deletion in multiselect', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2,option3',
|
||||||
|
multiSelect: true,
|
||||||
|
selectedValue: ['option1', 'option2', 'option3'],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const combobox = screen.getByRole('combobox');
|
||||||
|
await user.click(combobox);
|
||||||
|
|
||||||
|
// Navigate to chips using arrow keys
|
||||||
|
await user.keyboard('{ArrowLeft}');
|
||||||
|
|
||||||
|
// Use Shift + Arrow to navigate between chips
|
||||||
|
await user.keyboard('{Shift>}{ArrowLeft}{/Shift}');
|
||||||
|
|
||||||
|
// Use Del to delete the active chip
|
||||||
|
await user.keyboard('{Delete}');
|
||||||
|
|
||||||
|
// Note: The component may not immediately call onValueUpdate
|
||||||
|
// This test verifies the chip deletion behavior
|
||||||
|
await waitFor(() => {
|
||||||
|
// Check if a chip was removed from the selection
|
||||||
|
const selectionItems = document.querySelectorAll(
|
||||||
|
'.ant-select-selection-item',
|
||||||
|
);
|
||||||
|
expect(selectionItems.length).toBeLessThan(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 11. ADVANCED UI STATES =====
|
||||||
|
describe('Advanced UI States (VI)', () => {
|
||||||
|
test('VI-19: No data with previous value selected in variable', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: '',
|
||||||
|
multiSelect: true,
|
||||||
|
selectedValue: ['previous-value'],
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const combobox = screen.getByRole('combobox');
|
||||||
|
await user.click(combobox);
|
||||||
|
|
||||||
|
// Should show no data message (the component may not show this exact text)
|
||||||
|
await waitFor(() => {
|
||||||
|
// Check if dropdown is empty or shows no data indication
|
||||||
|
const dropdown = document.querySelector('.ant-select-dropdown');
|
||||||
|
expect(dropdown).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should still show the previous selected value
|
||||||
|
expect(screen.getByText('previous-value')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('VI-20: Always editable accessibility in variable', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2',
|
||||||
|
multiSelect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const combobox = screen.getByRole('combobox');
|
||||||
|
|
||||||
|
// Should be editable
|
||||||
|
expect(combobox).not.toBeDisabled();
|
||||||
|
await user.click(combobox);
|
||||||
|
expect(combobox).toHaveFocus();
|
||||||
|
|
||||||
|
// Should still be interactive
|
||||||
|
expect(combobox).not.toBeDisabled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 12. REGEX AND CUSTOM VALUES =====
|
||||||
|
describe('Regex and Custom Values (VI)', () => {
|
||||||
|
test('VI-22: Regex pattern support in variable', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2,option3',
|
||||||
|
multiSelect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const combobox = screen.getByRole('combobox');
|
||||||
|
await user.click(combobox);
|
||||||
|
|
||||||
|
// Wait for dropdown to open (don't expect ALL option as it might not be there)
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('option1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = document.querySelector(
|
||||||
|
'.ant-select-selection-search-input',
|
||||||
|
);
|
||||||
|
expect(searchInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
if (searchInput) {
|
||||||
|
// Test regex pattern
|
||||||
|
await user.type(searchInput, '.*test.*');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should create custom value for regex pattern
|
||||||
|
await waitFor(() => {
|
||||||
|
const regexOptions = screen.getAllByText('.*test.*');
|
||||||
|
expect(regexOptions.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Check that at least one has the Custom badge
|
||||||
|
const customBadgeElements = document.querySelectorAll('.option-badge');
|
||||||
|
const hasCustomBadge = Array.from(customBadgeElements).some(
|
||||||
|
(badge) => badge.textContent === 'Custom',
|
||||||
|
);
|
||||||
|
expect(hasCustomBadge).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Press Enter to create the regex value
|
||||||
|
await user.keyboard('{Enter}');
|
||||||
|
|
||||||
|
// Should create the regex value (verify it's in the selection)
|
||||||
|
await waitFor(() => {
|
||||||
|
const selectionItems = document.querySelectorAll(
|
||||||
|
'.ant-select-selection-item',
|
||||||
|
);
|
||||||
|
const hasRegexValue = Array.from(selectionItems).some((item) =>
|
||||||
|
item.textContent?.includes('.*test.*'),
|
||||||
|
);
|
||||||
|
expect(hasRegexValue).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== 13. DROPDOWN PERSISTENCE =====
|
||||||
|
describe('Dropdown Persistence (VI)', () => {
|
||||||
|
test('VI-24: Dropdown stays open for non-save actions in variable', async () => {
|
||||||
|
const variable = createMockVariable({
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2,option3',
|
||||||
|
multiSelect: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<TestWrapper>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variable}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={null}
|
||||||
|
/>
|
||||||
|
</TestWrapper>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const combobox = screen.getByRole('combobox');
|
||||||
|
await user.click(combobox);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Navigate with arrow keys (non-save action)
|
||||||
|
await user.keyboard('{ArrowDown}');
|
||||||
|
await user.keyboard('{ArrowDown}');
|
||||||
|
|
||||||
|
// Dropdown should still be open
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('option1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click on an option (selection action, not save)
|
||||||
|
const option1 = screen.getByText('option1');
|
||||||
|
await user.click(option1);
|
||||||
|
|
||||||
|
// Dropdown should still be open for more selections
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('option2')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const dropdown = document.querySelector('.ant-select-dropdown');
|
||||||
|
expect(dropdown).not.toHaveClass('ant-select-dropdown-hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only ESC should close the dropdown
|
||||||
|
await user.keyboard('{Escape}');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const dropdown = document.querySelector('.ant-select-dropdown');
|
||||||
|
expect(dropdown).toHaveClass('ant-select-dropdown-hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -656,6 +656,10 @@ $custom-border-color: #2c3044;
|
|||||||
border: 1px solid #e8e8e8;
|
border: 1px solid #e8e8e8;
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: rgba(0, 0, 0, 0.85);
|
||||||
|
|
||||||
|
font-size: 12px !important;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
.ant-select-selection-item-content {
|
.ant-select-selection-item-content {
|
||||||
color: rgba(0, 0, 0, 0.85);
|
color: rgba(0, 0, 0, 0.85);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export interface CustomSelectProps extends Omit<SelectProps, 'options'> {
|
|||||||
highlightSearch?: boolean;
|
highlightSearch?: boolean;
|
||||||
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||||
popupMatchSelectWidth?: boolean;
|
popupMatchSelectWidth?: boolean;
|
||||||
errorMessage?: string;
|
errorMessage?: string | null;
|
||||||
allowClear?: SelectProps['allowClear'];
|
allowClear?: SelectProps['allowClear'];
|
||||||
onRetry?: () => void;
|
onRetry?: () => void;
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@ export interface CustomMultiSelectProps
|
|||||||
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
|
getPopupContainer?: (triggerNode: HTMLElement) => HTMLElement;
|
||||||
dropdownRender?: (menu: React.ReactElement) => React.ReactElement;
|
dropdownRender?: (menu: React.ReactElement) => React.ReactElement;
|
||||||
highlightSearch?: boolean;
|
highlightSearch?: boolean;
|
||||||
errorMessage?: string;
|
errorMessage?: string | null;
|
||||||
popupClassName?: string;
|
popupClassName?: string;
|
||||||
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
placement?: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight';
|
||||||
maxTagCount?: number;
|
maxTagCount?: number;
|
||||||
|
|||||||
@ -167,7 +167,7 @@ describe('VariableItem', () => {
|
|||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByTitle('ALL')).toBeInTheDocument();
|
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('calls useEffect when the component mounts', () => {
|
test('calls useEffect when the component mounts', () => {
|
||||||
|
|||||||
@ -8,23 +8,14 @@ import './DashboardVariableSelection.styles.scss';
|
|||||||
|
|
||||||
import { orange } from '@ant-design/colors';
|
import { orange } from '@ant-design/colors';
|
||||||
import { InfoCircleOutlined, WarningOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined, WarningOutlined } from '@ant-design/icons';
|
||||||
import {
|
import { Input, Popover, Tooltip, Typography } from 'antd';
|
||||||
Checkbox,
|
|
||||||
Input,
|
|
||||||
Popover,
|
|
||||||
Select,
|
|
||||||
Tag,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
} from 'antd';
|
|
||||||
import { CheckboxChangeEvent } from 'antd/es/checkbox';
|
|
||||||
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
import dashboardVariablesQuery from 'api/dashboard/variables/dashboardVariablesQuery';
|
||||||
|
import { CustomMultiSelect, CustomSelect } from 'components/NewSelect';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
import { commaValuesParser } from 'lib/dashbaordVariables/customCommaValuesParser';
|
||||||
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
import sortValues from 'lib/dashbaordVariables/sortVariableValues';
|
||||||
import { debounce, isArray, isString } from 'lodash-es';
|
import { debounce, isArray, isString } from 'lodash-es';
|
||||||
import map from 'lodash-es/map';
|
import { memo, useEffect, useMemo, useState } from 'react';
|
||||||
import { ChangeEvent, memo, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -39,11 +30,6 @@ import { areArraysEqual, checkAPIInvocation, IDependencyData } from './util';
|
|||||||
|
|
||||||
const ALL_SELECT_VALUE = '__ALL__';
|
const ALL_SELECT_VALUE = '__ALL__';
|
||||||
|
|
||||||
enum ToggleTagValue {
|
|
||||||
Only = 'Only',
|
|
||||||
All = 'All',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VariableItemProps {
|
interface VariableItemProps {
|
||||||
variableData: IDashboardVariable;
|
variableData: IDashboardVariable;
|
||||||
existingVariables: Record<string, IDashboardVariable>;
|
existingVariables: Record<string, IDashboardVariable>;
|
||||||
@ -83,6 +69,9 @@ function VariableItem({
|
|||||||
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
const [optionsData, setOptionsData] = useState<(string | number | boolean)[]>(
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
const [tempSelection, setTempSelection] = useState<
|
||||||
|
string | string[] | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
@ -146,18 +135,10 @@ function VariableItem({
|
|||||||
variableData.name &&
|
variableData.name &&
|
||||||
(validVariableUpdate() || valueNotInList || variableData.allSelected)
|
(validVariableUpdate() || valueNotInList || variableData.allSelected)
|
||||||
) {
|
) {
|
||||||
let value = variableData.selectedValue;
|
const value = variableData.selectedValue;
|
||||||
let allSelected = false;
|
let allSelected = false;
|
||||||
// The default value for multi-select is ALL and first value for
|
|
||||||
// single select
|
if (variableData.multiSelect) {
|
||||||
if (valueNotInList) {
|
|
||||||
if (variableData.multiSelect) {
|
|
||||||
value = newOptionsData;
|
|
||||||
allSelected = true;
|
|
||||||
} else {
|
|
||||||
[value] = newOptionsData;
|
|
||||||
}
|
|
||||||
} else if (variableData.multiSelect) {
|
|
||||||
const { selectedValue } = variableData;
|
const { selectedValue } = variableData;
|
||||||
allSelected =
|
allSelected =
|
||||||
newOptionsData.length > 0 &&
|
newOptionsData.length > 0 &&
|
||||||
@ -265,6 +246,27 @@ function VariableItem({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add a handler for tracking temporary selection changes
|
||||||
|
const handleTempChange = (inputValue: string | string[]): void => {
|
||||||
|
// Store the selection in temporary state while dropdown is open
|
||||||
|
const value = variableData.multiSelect && !inputValue ? [] : inputValue;
|
||||||
|
setTempSelection(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle dropdown visibility changes
|
||||||
|
const handleDropdownVisibleChange = (visible: boolean): void => {
|
||||||
|
// Initialize temp selection when opening dropdown
|
||||||
|
if (visible) {
|
||||||
|
setTempSelection(getSelectValue(variableData.selectedValue, variableData));
|
||||||
|
}
|
||||||
|
// Apply changes when closing dropdown
|
||||||
|
else if (!visible && tempSelection !== undefined) {
|
||||||
|
// Call handleChange with the temporarily stored selection
|
||||||
|
handleChange(tempSelection);
|
||||||
|
setTempSelection(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// do not debounce the above function as we do not need debounce in select variables
|
// do not debounce the above function as we do not need debounce in select variables
|
||||||
const debouncedHandleChange = debounce(handleChange, 500);
|
const debouncedHandleChange = debounce(handleChange, 500);
|
||||||
|
|
||||||
@ -281,11 +283,6 @@ function VariableItem({
|
|||||||
? 'ALL'
|
? 'ALL'
|
||||||
: selectedValueStringified;
|
: selectedValueStringified;
|
||||||
|
|
||||||
const mode: 'multiple' | undefined =
|
|
||||||
variableData.multiSelect && !variableData.allSelected
|
|
||||||
? 'multiple'
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch options for CUSTOM Type
|
// Fetch options for CUSTOM Type
|
||||||
if (variableData.type === 'CUSTOM') {
|
if (variableData.type === 'CUSTOM') {
|
||||||
@ -294,113 +291,6 @@ function VariableItem({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [variableData.type, variableData.customValue]);
|
}, [variableData.type, variableData.customValue]);
|
||||||
|
|
||||||
const checkAll = (e: MouseEvent): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
const isChecked =
|
|
||||||
variableData.allSelected || selectValue?.includes(ALL_SELECT_VALUE);
|
|
||||||
|
|
||||||
if (isChecked) {
|
|
||||||
handleChange([]);
|
|
||||||
} else {
|
|
||||||
handleChange(ALL_SELECT_VALUE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOptionSelect = (
|
|
||||||
e: CheckboxChangeEvent,
|
|
||||||
option: string | number | boolean,
|
|
||||||
): void => {
|
|
||||||
const newSelectedValue = Array.isArray(selectedValue)
|
|
||||||
? ((selectedValue.filter(
|
|
||||||
(val) => val.toString() !== option.toString(),
|
|
||||||
) as unknown) as string[])
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!e.target.checked &&
|
|
||||||
Array.isArray(selectedValueStringified) &&
|
|
||||||
selectedValueStringified.includes(option.toString())
|
|
||||||
) {
|
|
||||||
if (newSelectedValue.length === 1) {
|
|
||||||
handleChange(newSelectedValue[0].toString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleChange(newSelectedValue);
|
|
||||||
} else if (!e.target.checked && selectedValue === option.toString()) {
|
|
||||||
handleChange(ALL_SELECT_VALUE);
|
|
||||||
} else if (newSelectedValue.length === optionsData.length - 1) {
|
|
||||||
handleChange(ALL_SELECT_VALUE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [optionState, setOptionState] = useState({
|
|
||||||
tag: '',
|
|
||||||
visible: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
function currentToggleTagValue({
|
|
||||||
option,
|
|
||||||
}: {
|
|
||||||
option: string;
|
|
||||||
}): ToggleTagValue {
|
|
||||||
if (
|
|
||||||
option.toString() === selectValue ||
|
|
||||||
(Array.isArray(selectValue) &&
|
|
||||||
selectValue?.includes(option.toString()) &&
|
|
||||||
selectValue.length === 1)
|
|
||||||
) {
|
|
||||||
return ToggleTagValue.All;
|
|
||||||
}
|
|
||||||
return ToggleTagValue.Only;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleToggle(e: ChangeEvent, option: string): void {
|
|
||||||
e.stopPropagation();
|
|
||||||
const mode = currentToggleTagValue({ option: option as string });
|
|
||||||
const isChecked =
|
|
||||||
variableData.allSelected ||
|
|
||||||
option.toString() === selectValue ||
|
|
||||||
(Array.isArray(selectValue) && selectValue?.includes(option.toString()));
|
|
||||||
|
|
||||||
if (isChecked) {
|
|
||||||
if (mode === ToggleTagValue.Only && variableData.multiSelect) {
|
|
||||||
handleChange([option.toString()]);
|
|
||||||
} else if (!variableData.multiSelect) {
|
|
||||||
handleChange(option.toString());
|
|
||||||
} else {
|
|
||||||
handleChange(ALL_SELECT_VALUE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
handleChange(option.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function retProps(
|
|
||||||
option: string,
|
|
||||||
): {
|
|
||||||
onMouseOver: () => void;
|
|
||||||
onMouseOut: () => void;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
onMouseOver: (): void =>
|
|
||||||
setOptionState({
|
|
||||||
tag: option.toString(),
|
|
||||||
visible: true,
|
|
||||||
}),
|
|
||||||
onMouseOut: (): void =>
|
|
||||||
setOptionState({
|
|
||||||
tag: option.toString(),
|
|
||||||
visible: false,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const ensureValidOption = (option: string): boolean =>
|
|
||||||
!(
|
|
||||||
currentToggleTagValue({ option }) === ToggleTagValue.All && !enableSelectAll
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="variable-item">
|
<div className="variable-item">
|
||||||
<Typography.Text className="variable-name" ellipsis>
|
<Typography.Text className="variable-name" ellipsis>
|
||||||
@ -428,9 +318,48 @@ function VariableItem({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
!errorMessage &&
|
optionsData &&
|
||||||
optionsData && (
|
(variableData.multiSelect ? (
|
||||||
<Select
|
<CustomMultiSelect
|
||||||
|
key={
|
||||||
|
selectValue && Array.isArray(selectValue)
|
||||||
|
? selectValue.join(' ')
|
||||||
|
: selectValue || variableData.id
|
||||||
|
}
|
||||||
|
options={optionsData.map((option) => ({
|
||||||
|
label: option.toString(),
|
||||||
|
value: option.toString(),
|
||||||
|
}))}
|
||||||
|
defaultValue={selectValue}
|
||||||
|
onChange={handleTempChange}
|
||||||
|
bordered={false}
|
||||||
|
placeholder="Select value"
|
||||||
|
placement="bottomLeft"
|
||||||
|
style={SelectItemStyle}
|
||||||
|
loading={isLoading}
|
||||||
|
showSearch
|
||||||
|
data-testid="variable-select"
|
||||||
|
className="variable-select"
|
||||||
|
popupClassName="dropdown-styles"
|
||||||
|
maxTagCount={4}
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
allowClear
|
||||||
|
value={tempSelection || selectValue}
|
||||||
|
onDropdownVisibleChange={handleDropdownVisibleChange}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
// eslint-disable-next-line react/no-unstable-nested-components
|
||||||
|
maxTagPlaceholder={(omittedValues): JSX.Element => (
|
||||||
|
<Tooltip title={omittedValues.map(({ value }) => value).join(', ')}>
|
||||||
|
<span>+ {omittedValues.length} </span>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
onClear={(): void => {
|
||||||
|
handleChange([]);
|
||||||
|
}}
|
||||||
|
enableAllSelection={enableSelectAll}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CustomSelect
|
||||||
key={
|
key={
|
||||||
selectValue && Array.isArray(selectValue)
|
selectValue && Array.isArray(selectValue)
|
||||||
? selectValue.join(' ')
|
? selectValue.join(' ')
|
||||||
@ -440,93 +369,21 @@ function VariableItem({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
bordered={false}
|
bordered={false}
|
||||||
placeholder="Select value"
|
placeholder="Select value"
|
||||||
placement="bottomLeft"
|
|
||||||
mode={mode}
|
|
||||||
style={SelectItemStyle}
|
style={SelectItemStyle}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
showSearch
|
showSearch
|
||||||
data-testid="variable-select"
|
data-testid="variable-select"
|
||||||
className="variable-select"
|
className="variable-select"
|
||||||
popupClassName="dropdown-styles"
|
popupClassName="dropdown-styles"
|
||||||
maxTagCount={4}
|
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
// eslint-disable-next-line react/no-unstable-nested-components
|
options={optionsData.map((option) => ({
|
||||||
tagRender={(props): JSX.Element => (
|
label: option.toString(),
|
||||||
<Tag closable onClose={props.onClose}>
|
value: option.toString(),
|
||||||
{props.value}
|
}))}
|
||||||
</Tag>
|
value={selectValue}
|
||||||
)}
|
errorMessage={errorMessage}
|
||||||
// eslint-disable-next-line react/no-unstable-nested-components
|
/>
|
||||||
maxTagPlaceholder={(omittedValues): JSX.Element => (
|
))
|
||||||
<Tooltip title={omittedValues.map(({ value }) => value).join(', ')}>
|
|
||||||
<span>+ {omittedValues.length} </span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
allowClear={selectValue !== ALL_SELECT_VALUE && selectValue !== 'ALL'}
|
|
||||||
>
|
|
||||||
{enableSelectAll && (
|
|
||||||
<Select.Option data-testid="option-ALL" value={ALL_SELECT_VALUE}>
|
|
||||||
<div className="all-label" onClick={(e): void => checkAll(e as any)}>
|
|
||||||
<Checkbox checked={variableData.allSelected} />
|
|
||||||
ALL
|
|
||||||
</div>
|
|
||||||
</Select.Option>
|
|
||||||
)}
|
|
||||||
{map(optionsData, (option) => (
|
|
||||||
<Select.Option
|
|
||||||
data-testid={`option-${option}`}
|
|
||||||
key={option.toString()}
|
|
||||||
value={option}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={variableData.multiSelect ? 'dropdown-checkbox-label' : ''}
|
|
||||||
>
|
|
||||||
{variableData.multiSelect && (
|
|
||||||
<Checkbox
|
|
||||||
onChange={(e): void => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
handleOptionSelect(e, option);
|
|
||||||
}}
|
|
||||||
checked={
|
|
||||||
variableData.allSelected ||
|
|
||||||
option.toString() === selectValue ||
|
|
||||||
(Array.isArray(selectValue) &&
|
|
||||||
selectValue?.includes(option.toString()))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className="dropdown-value"
|
|
||||||
{...retProps(option as string)}
|
|
||||||
onClick={(e): void => handleToggle(e as any, option as string)}
|
|
||||||
>
|
|
||||||
<Typography.Text
|
|
||||||
ellipsis={{
|
|
||||||
tooltip: {
|
|
||||||
placement: variableData.multiSelect ? 'top' : 'right',
|
|
||||||
autoAdjustOverflow: true,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
className="option-text"
|
|
||||||
>
|
|
||||||
{option.toString()}
|
|
||||||
</Typography.Text>
|
|
||||||
|
|
||||||
{variableData.multiSelect &&
|
|
||||||
optionState.tag === option.toString() &&
|
|
||||||
optionState.visible &&
|
|
||||||
ensureValidOption(option as string) && (
|
|
||||||
<Typography.Text className="toggle-tag-label">
|
|
||||||
{currentToggleTagValue({ option: option as string })}
|
|
||||||
</Typography.Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
{variableData.type !== 'TEXTBOX' && errorMessage && (
|
{variableData.type !== 'TEXTBOX' && errorMessage && (
|
||||||
<span style={{ margin: '0 0.5rem' }}>
|
<span style={{ margin: '0 0.5rem' }}>
|
||||||
|
|||||||
@ -0,0 +1,140 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
|
||||||
|
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
|
import { render, screen, waitFor } from 'tests/test-utils';
|
||||||
|
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
|
import VariableItem from '../VariableItem';
|
||||||
|
|
||||||
|
const mockOnValueUpdate = jest.fn();
|
||||||
|
const mockSetVariablesToGetUpdated = jest.fn();
|
||||||
|
|
||||||
|
const baseDependencyData = {
|
||||||
|
order: [],
|
||||||
|
graph: {},
|
||||||
|
parentDependencyGraph: {},
|
||||||
|
hasCycle: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEST_VARIABLE_ID = 'test_variable';
|
||||||
|
const VARIABLE_SELECT_TESTID = 'variable-select';
|
||||||
|
const TEST_VARIABLE_NAME = 'testVariable';
|
||||||
|
const TEST_VARIABLE_DESCRIPTION = 'Test Variable';
|
||||||
|
|
||||||
|
const renderVariableItem = (
|
||||||
|
variableData: IDashboardVariable,
|
||||||
|
): ReturnType<typeof render> =>
|
||||||
|
render(
|
||||||
|
<MockQueryClientProvider>
|
||||||
|
<VariableItem
|
||||||
|
variableData={variableData}
|
||||||
|
existingVariables={{}}
|
||||||
|
onValueUpdate={mockOnValueUpdate}
|
||||||
|
variablesToGetUpdated={[]}
|
||||||
|
setVariablesToGetUpdated={mockSetVariablesToGetUpdated}
|
||||||
|
dependencyData={baseDependencyData}
|
||||||
|
/>
|
||||||
|
</MockQueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('VariableItem Default Value Selection Behavior', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Single Select Variables', () => {
|
||||||
|
test('should keep previous selection value', async () => {
|
||||||
|
const variable: IDashboardVariable = {
|
||||||
|
id: TEST_VARIABLE_ID,
|
||||||
|
name: TEST_VARIABLE_NAME,
|
||||||
|
description: TEST_VARIABLE_DESCRIPTION,
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2,option3',
|
||||||
|
selectedValue: 'option1',
|
||||||
|
sort: 'DISABLED',
|
||||||
|
multiSelect: false,
|
||||||
|
showALLOption: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderVariableItem(variable);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId(VARIABLE_SELECT_TESTID)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('option1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show placeholder when no previous and no default', async () => {
|
||||||
|
const variable: IDashboardVariable = {
|
||||||
|
id: TEST_VARIABLE_ID,
|
||||||
|
name: TEST_VARIABLE_NAME,
|
||||||
|
description: TEST_VARIABLE_DESCRIPTION,
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2,option3',
|
||||||
|
selectedValue: undefined,
|
||||||
|
sort: 'DISABLED',
|
||||||
|
multiSelect: false,
|
||||||
|
showALLOption: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderVariableItem(variable);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId(VARIABLE_SELECT_TESTID)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('Select value')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Multi Select Variables with ALL enabled', () => {
|
||||||
|
test('should show ALL when all options are selected', async () => {
|
||||||
|
const variable: IDashboardVariable = {
|
||||||
|
id: TEST_VARIABLE_ID,
|
||||||
|
name: TEST_VARIABLE_NAME,
|
||||||
|
description: TEST_VARIABLE_DESCRIPTION,
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2,option3',
|
||||||
|
selectedValue: ['option1', 'option2', 'option3'],
|
||||||
|
allSelected: true,
|
||||||
|
sort: 'DISABLED',
|
||||||
|
multiSelect: true,
|
||||||
|
showALLOption: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderVariableItem(variable);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId(VARIABLE_SELECT_TESTID)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('ALL')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Multi Select Variables with ALL disabled', () => {
|
||||||
|
test('should show placeholder when no selection', async () => {
|
||||||
|
const variable: IDashboardVariable = {
|
||||||
|
id: TEST_VARIABLE_ID,
|
||||||
|
name: TEST_VARIABLE_NAME,
|
||||||
|
description: TEST_VARIABLE_DESCRIPTION,
|
||||||
|
type: 'CUSTOM',
|
||||||
|
customValue: 'option1,option2,option3',
|
||||||
|
selectedValue: undefined,
|
||||||
|
sort: 'DISABLED',
|
||||||
|
multiSelect: true,
|
||||||
|
showALLOption: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderVariableItem(variable);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId(VARIABLE_SELECT_TESTID)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.getByText('Select value')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user