fix(superset-ui-core): achieve 100% coverage for npm run core:cover (#38397)

This commit is contained in:
Michael S. Molina
2026-03-04 13:56:51 -03:00
committed by GitHub
parent 35d0aad854
commit 69732d9dca
20 changed files with 1373 additions and 31 deletions

View File

@@ -45,6 +45,14 @@ describe('metricColumnFilter', () => {
}) as SqlaFormData;
describe('shouldSkipMetricColumn', () => {
test('should return false for empty colname', () => {
const colnames = ['metric1', '%metric1'];
const formData = createFormData([], ['metric1']);
expect(shouldSkipMetricColumn({ colname: '', colnames, formData })).toBe(
false,
);
});
test('should skip unprefixed percent metric columns if prefixed version exists', () => {
const colnames = ['metric1', '%metric1'];
const formData = createFormData([], ['metric1']);

View File

@@ -506,6 +506,19 @@ test('getColorFunction IsNotNull', () => {
expect(colorFunction(null)).toBeUndefined();
});
test('getColorFunction IsNotNull returns undefined for non-boolean value', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.IsNotNull,
targetValue: '',
colorScheme: '#FF0000',
column: 'isMember',
},
boolValues,
);
expect(colorFunction(50 as unknown as boolean)).toBeUndefined();
});
test('getColorFunction returns undefined for null values on numeric comparators', () => {
const operators = [
{ operator: Comparator.LessThan, targetValue: 50 },
@@ -805,6 +818,34 @@ test('getColorFormatters with useGradient flag', () => {
expect(colorFormatters[1].getColorFromValue(100)).toEqual('#00FF00FF');
});
test('getColorFunction NOT_EQUAL returns undefined when targetValue is non-numeric', () => {
const colorFunction = getColorFunction(
{
operator: Comparator.NotEqual,
targetValue: 'not-a-number' as unknown as number,
colorScheme: '#FF0000',
column: 'count',
},
countValues,
);
expect(colorFunction(50)).toBeUndefined();
expect(colorFunction(100)).toBeUndefined();
});
test('getColorFormatters resolves colorScheme from theme when it starts with "color"', () => {
const theme = { colorPrimary: '#AABBCC' };
const columnConfig = [
{
operator: Comparator.None,
colorScheme: 'colorPrimary',
column: 'count',
},
];
const colorFormatters = getColorFormatters(columnConfig, mockData, theme);
expect(colorFormatters).toHaveLength(1);
expect(colorFormatters[0].getColorFromValue(75)).toContain('#AABBCC');
});
test('correct column boolean config', () => {
const columnConfigBoolean = [
{

View File

@@ -294,6 +294,126 @@ test('should preserve existing adhoc filters', () => {
);
});
test('should return null when no matrixify configuration exists', () => {
const formData: TestFormData = {
viz_type: 'table',
datasource: '1__table',
// No matrixify_mode_rows or matrixify_mode_columns
};
const grid = generateMatrixifyGrid(formData);
expect(grid).toBeNull();
});
test('should generate single-column grid when only rows are configured', () => {
const rowsOnlyFormData: TestFormData = {
viz_type: 'table',
datasource: '1__table',
matrixify_mode_rows: 'metrics',
matrixify_rows: [createAdhocMetric('Revenue'), createAdhocMetric('Profit')],
// No column config
};
const grid = generateMatrixifyGrid(rowsOnlyFormData);
expect(grid).not.toBeNull();
expect(grid!.rowHeaders).toEqual(['Revenue', 'Profit']);
expect(grid!.colHeaders).toEqual(['']);
expect(grid!.cells).toHaveLength(2);
expect(grid!.cells[0]).toHaveLength(1);
});
test('should generate single-row grid when only columns are configured', () => {
const colsOnlyFormData: TestFormData = {
viz_type: 'table',
datasource: '1__table',
matrixify_mode_columns: 'metrics',
matrixify_columns: [
createSqlMetric('Q1', 'SUM(q1)'),
createSqlMetric('Q2', 'SUM(q2)'),
],
// No row config
};
const grid = generateMatrixifyGrid(colsOnlyFormData);
expect(grid).not.toBeNull();
expect(grid!.rowHeaders).toEqual(['']);
expect(grid!.colHeaders).toEqual(['Q1', 'Q2']);
expect(grid!.cells).toHaveLength(1);
expect(grid!.cells[0]).toHaveLength(2);
});
test('should handle invalid Handlebars template gracefully', () => {
const formDataWithBadTemplate: TestFormData = {
...baseFormData,
matrixify_cell_title_template: '{{#if}}unclosed',
};
const grid = generateMatrixifyGrid(formDataWithBadTemplate);
expect(grid).not.toBeNull();
// Should not throw - returns empty title on template error
const firstCell = grid!.cells[0][0];
expect(firstCell!.title).toBe('');
});
test('should return empty string header for null metric in array (line 76)', () => {
const formData: TestFormData = {
viz_type: 'table',
datasource: '1__table',
matrixify_mode_rows: 'metrics',
matrixify_mode_columns: 'metrics',
matrixify_rows: [null],
matrixify_columns: [createAdhocMetric('Q1')],
};
const grid = generateMatrixifyGrid(formData);
expect(grid).not.toBeNull();
expect(grid!.rowHeaders).toEqual(['']);
});
test('should return empty string header for empty-string dimension value (line 86)', () => {
const formData: TestFormData = {
viz_type: 'table',
datasource: '1__table',
matrixify_mode_rows: 'dimensions',
matrixify_mode_columns: 'dimensions',
matrixify_dimension_rows: { dimension: 'country', values: [''] },
matrixify_dimension_columns: { dimension: 'product', values: ['Widget'] },
};
const grid = generateMatrixifyGrid(formData);
expect(grid).not.toBeNull();
expect(grid!.rowHeaders).toEqual(['']);
});
test('should skip dimension filter when value is undefined (lines 151, 165)', () => {
const formData: TestFormData = {
viz_type: 'table',
datasource: '1__table',
matrixify_mode_rows: 'dimensions',
matrixify_mode_columns: 'dimensions',
matrixify_dimension_rows: {
dimension: 'country',
values: [undefined, 'USA'],
},
matrixify_dimension_columns: {
dimension: 'product',
values: [undefined, 'Widget'],
},
};
const grid = generateMatrixifyGrid(formData);
expect(grid).not.toBeNull();
// Cell at row=0, col=0 has undefined values on both axes — no filters applied
const cell00 = grid!.cells[0][0];
expect(cell00).toBeDefined();
expect(cell00!.formData.adhoc_filters ?? []).toEqual([]);
// Cell at row=1, col=1 has defined values — filters applied
const cell11 = grid!.cells[1][1];
expect(cell11!.formData.adhoc_filters).toEqual(
expect.arrayContaining([
expect.objectContaining({ subject: 'country', comparator: 'USA' }),
expect.objectContaining({ subject: 'product', comparator: 'Widget' }),
]),
);
});
test('should handle metrics without labels', () => {
const metricsWithoutLabels: TestFormData = {
viz_type: 'table',

View File

@@ -276,10 +276,10 @@ export function generateMatrixifyGrid(
const cellFormData = generateCellFormData(
formData,
rowCount > 0 ? config.rows : null,
colCount > 0 ? config.columns : null,
rowCount > 0 ? row : null,
colCount > 0 ? col : null,
config.rows,
config.columns,
row,
col,
);
// Generate title using template if provided

View File

@@ -0,0 +1,28 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { isMatrixifyEnabled, MatrixifyGridRenderer } from './matrixify.mocks';
test('isMatrixifyEnabled mock returns false by default', () => {
expect(isMatrixifyEnabled()).toBe(false);
});
test('MatrixifyGridRenderer mock returns null by default', () => {
expect(MatrixifyGridRenderer()).toBeNull();
});

View File

@@ -260,6 +260,88 @@ test('should handle empty form data object', () => {
expect(isMatrixifyEnabled(formData)).toBe(false);
});
test('isMatrixifyEnabled should return false when layout enabled but no axis modes configured', () => {
const formData = {
viz_type: 'table',
matrixify_enable_vertical_layout: true,
// No matrixify_mode_rows or matrixify_mode_columns set
} as MatrixifyFormData;
expect(isMatrixifyEnabled(formData)).toBe(false);
});
test('getMatrixifyValidationErrors should return dimension error for rows when dimension has no data', () => {
const formData = {
viz_type: 'table',
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'dimensions',
// No matrixify_dimension_rows set
matrixify_mode_columns: 'metrics',
matrixify_columns: [createMetric('Q1')],
} as MatrixifyFormData;
const errors = getMatrixifyValidationErrors(formData);
expect(errors).toContain('Please select a dimension and values for rows');
});
test('getMatrixifyValidationErrors should return metric error for columns when metrics array is empty', () => {
const formData = {
viz_type: 'table',
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'metrics',
matrixify_rows: [createMetric('Revenue')],
matrixify_mode_columns: 'metrics',
matrixify_columns: [],
} as MatrixifyFormData;
const errors = getMatrixifyValidationErrors(formData);
expect(errors).toContain('Please select at least one metric for columns');
});
test('getMatrixifyValidationErrors should return dimension error for columns when no dimension data', () => {
const formData = {
viz_type: 'table',
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'metrics',
matrixify_rows: [createMetric('Revenue')],
matrixify_mode_columns: 'dimensions',
// No matrixify_dimension_columns set
} as MatrixifyFormData;
const errors = getMatrixifyValidationErrors(formData);
expect(errors).toContain('Please select a dimension and values for columns');
});
test('getMatrixifyValidationErrors skips row check when matrixify_mode_rows is not set (line 240 false, line 279 || false)', () => {
const formData = {
viz_type: 'table',
matrixify_enable_vertical_layout: true,
// No matrixify_mode_rows — hasRowMode = false
matrixify_mode_columns: 'metrics',
matrixify_columns: [createMetric('Q1')],
} as MatrixifyFormData;
const errors = getMatrixifyValidationErrors(formData);
expect(errors).toEqual([]);
});
test('getMatrixifyValidationErrors evaluates full && expression when dimension is set but values are empty (lines 244, 264, 283, 291 true branches)', () => {
const formData = {
viz_type: 'table',
matrixify_enable_vertical_layout: true,
matrixify_mode_rows: 'dimensions',
matrixify_dimension_rows: { dimension: 'country', values: [] },
matrixify_mode_columns: 'dimensions',
matrixify_dimension_columns: { dimension: 'product', values: [] },
} as MatrixifyFormData;
const errors = getMatrixifyValidationErrors(formData);
expect(errors).toContain('Please select a dimension and values for rows');
expect(errors).toContain('Please select a dimension and values for columns');
expect(errors).toContain(
'Configure at least one complete row or column axis',
);
});
test('should handle partial configuration with one axis only', () => {
const formData = {
viz_type: 'table',

View File

@@ -276,28 +276,24 @@ export function getMatrixifyValidationErrors(
}
// Must have at least one valid axis
if (hasRowMode || hasColumnMode) {
const hasRowData =
config.rows.mode === 'metrics'
? config.rows.metrics && config.rows.metrics.length > 0
: config.rows.dimension?.dimension &&
(config.rows.selectionMode === 'topn' ||
(config.rows.dimension.values &&
config.rows.dimension.values.length > 0));
const hasAnyRowData =
config.rows.mode === 'metrics'
? config.rows.metrics && config.rows.metrics.length > 0
: config.rows.dimension?.dimension &&
(config.rows.selectionMode === 'topn' ||
(config.rows.dimension.values &&
config.rows.dimension.values.length > 0));
const hasColumnData =
config.columns.mode === 'metrics'
? config.columns.metrics && config.columns.metrics.length > 0
: config.columns.dimension?.dimension &&
(config.columns.selectionMode === 'topn' ||
(config.columns.dimension.values &&
config.columns.dimension.values.length > 0));
const hasAnyColumnData =
config.columns.mode === 'metrics'
? config.columns.metrics && config.columns.metrics.length > 0
: config.columns.dimension?.dimension &&
(config.columns.selectionMode === 'topn' ||
(config.columns.dimension.values &&
config.columns.dimension.values.length > 0));
if (!hasRowData && !hasColumnData) {
errors.push('Configure at least one complete row or column axis');
}
} else {
errors.push('Please configure at least one row or column axis');
if (!hasAnyRowData && !hasAnyColumnData) {
errors.push('Configure at least one complete row or column axis');
}
return errors;

View File

@@ -72,4 +72,29 @@ describe('useJsonValidation', () => {
expect(result.current[0].text).toContain('Custom error');
});
test('falls back to "syntax error" when thrown error has no message (line 59 || branch)', () => {
const spy = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => {
throw {}; // no .message property → error.message is undefined → falsy
});
const { result } = renderHook(() => useJsonValidation('some invalid json'));
spy.mockRestore();
expect(result.current).toHaveLength(1);
expect(result.current[0].text).toContain('syntax error');
});
test('extracts row and column from error when message contains (line X column Y)', () => {
const spy = jest.spyOn(JSON, 'parse').mockImplementationOnce(() => {
throw new SyntaxError('Unexpected token (line 3 column 5)');
});
const { result } = renderHook(() => useJsonValidation('some invalid json'));
spy.mockRestore();
expect(result.current).toHaveLength(1);
expect(result.current[0].row).toBe(2); // 3 - 1 = 2 (0-based)
expect(result.current[0].column).toBe(4); // 5 - 1 = 4 (0-based)
});
});

View File

@@ -40,3 +40,13 @@ test('should render the correct number of items', () => {
expect(item).toHaveTextContent(`Item ${index + 1}`);
});
});
test('should render List.Item with compact prop', () => {
const { container } = render(<List.Item compact>Compact content</List.Item>);
expect(container).toBeInTheDocument();
});
test('should render List.Item without compact prop', () => {
const { container } = render(<List.Item>Regular content</List.Item>);
expect(container).toBeInTheDocument();
});

View File

@@ -0,0 +1,49 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { LabeledValue as AntdLabeledValue } from 'antd/es/select';
import { DEFAULT_SORT_COMPARATOR } from './constants';
test('DEFAULT_SORT_COMPARATOR sorts by label text when both labels are strings', () => {
const a = { value: 'b', label: 'banana' } as AntdLabeledValue;
const b = { value: 'a', label: 'apple' } as AntdLabeledValue;
expect(DEFAULT_SORT_COMPARATOR(a, b)).toBeGreaterThan(0);
expect(DEFAULT_SORT_COMPARATOR(b, a)).toBeLessThan(0);
});
test('DEFAULT_SORT_COMPARATOR sorts by value text when labels are not strings', () => {
const a = { value: 'b' } as AntdLabeledValue;
const b = { value: 'a' } as AntdLabeledValue;
expect(DEFAULT_SORT_COMPARATOR(a, b)).toBeGreaterThan(0);
expect(DEFAULT_SORT_COMPARATOR(b, a)).toBeLessThan(0);
});
test('DEFAULT_SORT_COMPARATOR returns numeric difference when values are numbers', () => {
const a = { value: 3 } as unknown as AntdLabeledValue;
const b = { value: 1 } as unknown as AntdLabeledValue;
expect(DEFAULT_SORT_COMPARATOR(a, b)).toBe(2);
expect(DEFAULT_SORT_COMPARATOR(b, a)).toBe(-2);
});
test('DEFAULT_SORT_COMPARATOR uses rankedSearchCompare when search is provided', () => {
const a = { value: 'abc', label: 'abc' } as AntdLabeledValue;
const b = { value: 'bc', label: 'bc' } as AntdLabeledValue;
// 'bc' is an exact match to search 'bc', so it should sort first (lower index = negative diff)
expect(DEFAULT_SORT_COMPARATOR(a, b, 'bc')).toBeGreaterThan(0);
});

View File

@@ -0,0 +1,574 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { SUPERSET_TABLE_COLUMN } from '..';
import InteractiveTableUtils from './InteractiveTableUtils';
const mockColumns = [
{ key: 'name', dataIndex: 'name', title: 'Name' },
{ key: 'age', dataIndex: 'age', title: 'Age' },
];
const createMockTable = (numCols = 2): HTMLTableElement => {
const table = document.createElement('table');
const thead = document.createElement('thead');
const tr = document.createElement('tr');
for (let i = 0; i < numCols; i += 1) {
const th = document.createElement('th');
tr.appendChild(th);
}
thead.appendChild(tr);
table.appendChild(thead);
document.body.appendChild(table);
return table;
};
afterEach(() => {
document.body.innerHTML = '';
});
test('constructor initializes with correct defaults', () => {
const table = createMockTable();
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
setDerivedColumns,
);
expect(utils.tableRef).toBe(table);
expect(utils.isDragging).toBe(false);
expect(utils.resizable).toBe(false);
expect(utils.reorderable).toBe(false);
expect(utils.derivedColumns).toEqual(mockColumns);
expect(utils.RESIZE_INDICATOR_THRESHOLD).toBe(8);
});
test('setTableRef updates tableRef', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const newTable = createMockTable();
utils.setTableRef(newTable);
expect(utils.tableRef).toBe(newTable);
});
test('getColumnIndex returns -1 when columnRef has no parent', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.columnRef = null;
expect(utils.getColumnIndex()).toBe(-1);
});
test('getColumnIndex returns correct index when columnRef is in a row', () => {
const table = createMockTable(3);
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const row = table.rows[0];
utils.columnRef = row.cells[1] as unknown as typeof utils.columnRef;
expect(utils.getColumnIndex()).toBe(1);
});
test('allowDrop calls preventDefault on the event', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const event = { preventDefault: jest.fn() } as unknown as DragEvent;
utils.allowDrop(event);
expect(event.preventDefault).toHaveBeenCalledTimes(1);
});
test('handleMouseup clears mouseDown and resets dragging state', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const th = document.createElement('th') as unknown as typeof utils.columnRef;
utils.columnRef = th;
(th as any).mouseDown = true;
utils.isDragging = true;
utils.handleMouseup();
expect((th as any).mouseDown).toBe(false);
expect(utils.isDragging).toBe(false);
});
test('handleMouseup works when columnRef is null', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.columnRef = null;
utils.isDragging = true;
utils.handleMouseup();
expect(utils.isDragging).toBe(false);
});
test('handleMouseDown sets mouseDown and oldX when within resize range', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const target = document.createElement('th') as any;
Object.defineProperty(target, 'offsetWidth', {
value: 100,
configurable: true,
});
const event = {
currentTarget: target,
offsetX: 95, // 100 - 95 = 5, within threshold of 8
x: 95,
} as unknown as MouseEvent;
utils.handleMouseDown(event);
expect(target.mouseDown).toBe(true);
expect(target.oldX).toBe(95);
expect(target.oldWidth).toBe(100);
expect(target.draggable).toBe(false);
});
test('handleMouseDown sets draggable when outside resize range and reorderable', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.reorderable = true;
const target = document.createElement('th') as any;
Object.defineProperty(target, 'offsetWidth', {
value: 100,
configurable: true,
});
const event = {
currentTarget: target,
offsetX: 50, // 100 - 50 = 50, outside threshold of 8
x: 50,
} as unknown as MouseEvent;
utils.handleMouseDown(event);
expect(target.draggable).toBe(true);
});
test('initializeResizableColumns adds event listeners when resizable is true', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const cell = table.rows[0].cells[0];
const addEventSpy = jest.spyOn(cell, 'addEventListener');
utils.initializeResizableColumns(true, table);
expect(utils.resizable).toBe(true);
expect(addEventSpy).toHaveBeenCalledWith('mousedown', utils.handleMouseDown);
expect(addEventSpy).toHaveBeenCalledWith(
'mousemove',
utils.handleMouseMove,
true,
);
});
test('initializeResizableColumns removes event listeners when resizable is false', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const cell = table.rows[0].cells[0];
const removeEventSpy = jest.spyOn(cell, 'removeEventListener');
utils.initializeResizableColumns(false, table);
expect(utils.resizable).toBe(false);
expect(removeEventSpy).toHaveBeenCalledWith(
'mousedown',
utils.handleMouseDown,
);
expect(removeEventSpy).toHaveBeenCalledWith(
'mousemove',
utils.handleMouseMove,
true,
);
});
test('initializeDragDropColumns adds event listeners when reorderable is true', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const cell = table.rows[0].cells[0];
const addEventSpy = jest.spyOn(cell, 'addEventListener');
utils.initializeDragDropColumns(true, table);
expect(utils.reorderable).toBe(true);
expect(addEventSpy).toHaveBeenCalledWith(
'dragstart',
utils.handleColumnDragStart,
);
expect(addEventSpy).toHaveBeenCalledWith('drop', utils.handleDragDrop);
});
test('initializeDragDropColumns removes event listeners when reorderable is false', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const cell = table.rows[0].cells[0];
const removeEventSpy = jest.spyOn(cell, 'removeEventListener');
utils.initializeDragDropColumns(false, table);
expect(utils.reorderable).toBe(false);
expect(removeEventSpy).toHaveBeenCalledWith(
'dragstart',
utils.handleColumnDragStart,
);
expect(removeEventSpy).toHaveBeenCalledWith('drop', utils.handleDragDrop);
});
test('handleColumnDragStart sets isDragging and calls setData', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const row = table.rows[0];
const target = row.cells[0] as any;
const setDataMock = jest.fn();
const event = {
currentTarget: target,
dataTransfer: { setData: setDataMock },
} as unknown as DragEvent;
utils.handleColumnDragStart(event);
expect(utils.isDragging).toBe(true);
expect(setDataMock).toHaveBeenCalledWith(
SUPERSET_TABLE_COLUMN,
expect.any(String),
);
});
test('handleDragDrop reorders columns when valid drag data exists', () => {
const table = createMockTable(2);
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
setDerivedColumns,
);
const row = table.rows[0];
// Set columnRef to first column (drag source)
utils.columnRef = row.cells[0] as unknown as typeof utils.columnRef;
const dragData = JSON.stringify({ index: 0, columnData: mockColumns[0] });
const dropTarget = row.cells[1];
const event = {
currentTarget: dropTarget,
dataTransfer: { getData: jest.fn().mockReturnValue(dragData) },
preventDefault: jest.fn(),
} as unknown as DragEvent;
utils.handleDragDrop(event);
expect(event.preventDefault).toHaveBeenCalledTimes(1);
expect(setDerivedColumns).toHaveBeenCalledTimes(1);
});
test('handleDragDrop does nothing when no drag data', () => {
const table = createMockTable(2);
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
setDerivedColumns,
);
const row = table.rows[0];
const event = {
currentTarget: row.cells[0],
dataTransfer: { getData: jest.fn().mockReturnValue('') },
preventDefault: jest.fn(),
} as unknown as DragEvent;
utils.handleDragDrop(event);
expect(event.preventDefault).not.toHaveBeenCalled();
expect(setDerivedColumns).not.toHaveBeenCalled();
});
test('handleMouseMove updates cursor to col-resize when within resize range', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.resizable = true;
const target = document.createElement('th') as any;
Object.defineProperty(target, 'offsetWidth', {
value: 100,
configurable: true,
});
target.style = { cursor: '' };
const event = {
currentTarget: target,
offsetX: 95,
x: 0,
} as unknown as MouseEvent;
utils.handleMouseMove(event);
expect(target.style.cursor).toBe('col-resize');
});
test('handleMouseMove sets default cursor when outside resize range', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.resizable = true;
const target = document.createElement('th') as any;
Object.defineProperty(target, 'offsetWidth', {
value: 100,
configurable: true,
});
target.style = { cursor: '' };
const event = {
currentTarget: target,
offsetX: 50,
x: 0,
} as unknown as MouseEvent;
utils.handleMouseMove(event);
expect(target.style.cursor).toBe('default');
});
test('handleMouseMove resizes column when mouseDown and within bounds', () => {
const table = createMockTable(2);
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
setDerivedColumns,
);
utils.resizable = true;
const row = table.rows[0];
const col = row.cells[0] as any;
col.mouseDown = true;
col.oldWidth = 100;
col.oldX = 50;
utils.columnRef = col;
const target = document.createElement('th') as any;
Object.defineProperty(target, 'offsetWidth', {
value: 100,
configurable: true,
});
target.style = { cursor: '' };
const event = {
currentTarget: target,
offsetX: 50,
x: 70, // diff = 70 - 50 = 20, width = 100 + 20 = 120
} as unknown as MouseEvent;
utils.handleMouseMove(event);
expect(setDerivedColumns).toHaveBeenCalledTimes(1);
expect(utils.derivedColumns[0].width).toBe(120);
});
test('handleMouseMove skips resize when not resizable', () => {
const table = createMockTable(2);
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
setDerivedColumns,
);
utils.resizable = false;
const target = document.createElement('th') as any;
const event = {
currentTarget: target,
offsetX: 50,
x: 70,
} as unknown as MouseEvent;
utils.handleMouseMove(event);
expect(setDerivedColumns).not.toHaveBeenCalled();
});
test('handleMouseMove handles negative diff by keeping original width', () => {
const table = createMockTable(2);
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
setDerivedColumns,
);
utils.resizable = true;
const row = table.rows[0];
const col = row.cells[0] as any;
col.mouseDown = true;
col.oldWidth = 50;
col.oldX = 200;
utils.columnRef = col;
const target = document.createElement('th') as any;
Object.defineProperty(target, 'offsetWidth', {
value: 50,
configurable: true,
});
target.style = { cursor: '' };
const event = {
currentTarget: target,
offsetX: 45,
x: 0, // diff = 0 - 200 = -200, width would be 50 + (-200) = -150 < 0 → keep 50
} as unknown as MouseEvent;
utils.handleMouseMove(event);
expect(setDerivedColumns).toHaveBeenCalledTimes(1);
expect(utils.derivedColumns[0].width).toBe(50); // unchanged because negative would result
});
test('handleColumnDragStart does not set columnRef when currentTarget is null (line 82 false)', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const event = {
currentTarget: null,
dataTransfer: { setData: jest.fn() },
} as unknown as DragEvent;
utils.handleColumnDragStart(event);
expect(utils.isDragging).toBe(true);
expect(utils.columnRef).toBeFalsy();
});
test('handleMouseDown does nothing when currentTarget is null (line 118 false)', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const event = {
currentTarget: null,
offsetX: 50,
x: 50,
} as unknown as MouseEvent;
utils.handleMouseDown(event);
expect(utils.columnRef).toBeFalsy();
});
test('handleMouseDown does nothing to draggable when outside resize range and not reorderable (line 132 false)', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.reorderable = false;
const target = document.createElement('th') as any;
Object.defineProperty(target, 'offsetWidth', {
value: 100,
configurable: true,
});
const event = {
currentTarget: target,
offsetX: 50, // 100 - 50 = 50, outside threshold of 8
x: 50,
} as unknown as MouseEvent;
utils.handleMouseDown(event);
expect(target.draggable).toBe(false);
});
test('handleMouseMove skips column update when getColumnIndex returns NaN (line 162 false)', () => {
const table = createMockTable(2);
const setDerivedColumns = jest.fn();
const utils = new InteractiveTableUtils(
table,
mockColumns,
setDerivedColumns,
);
utils.resizable = true;
const row = table.rows[0];
const col = row.cells[0] as any;
col.mouseDown = true;
col.oldWidth = 100;
col.oldX = 50;
utils.columnRef = col;
jest.spyOn(utils, 'getColumnIndex').mockReturnValueOnce(NaN);
const target = document.createElement('th') as any;
Object.defineProperty(target, 'offsetWidth', {
value: 100,
configurable: true,
});
target.style = { cursor: '' };
const event = {
currentTarget: target,
offsetX: 50,
x: 70,
} as unknown as MouseEvent;
utils.handleMouseMove(event);
expect(setDerivedColumns).not.toHaveBeenCalled();
});
test('initializeResizableColumns does nothing when table is null (lines 182-187 false)', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
expect(() => utils.initializeResizableColumns(true, null)).not.toThrow();
expect(utils.tableRef).toBeNull();
});
test('initializeResizableColumns uses default resizable=false when first arg is undefined (line 182 default branch)', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.initializeResizableColumns(undefined, table);
expect(utils.resizable).toBe(false);
});
test('initializeDragDropColumns does nothing when table is null (lines 206-211 false)', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
expect(() => utils.initializeDragDropColumns(true, null)).not.toThrow();
expect(utils.tableRef).toBeNull();
});
test('initializeDragDropColumns uses default reorderable=false when first arg is undefined (line 206 default branch)', () => {
const table = createMockTable(2);
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
utils.initializeDragDropColumns(undefined, table);
expect(utils.reorderable).toBe(false);
});
test('clearListeners removes document mouseup listener', () => {
const table = createMockTable();
const utils = new InteractiveTableUtils(table, mockColumns, jest.fn());
const removeEventSpy = jest.spyOn(document, 'removeEventListener');
utils.clearListeners();
expect(removeEventSpy).toHaveBeenCalledWith('mouseup', utils.handleMouseup);
});

View File

@@ -25,10 +25,3 @@ declare module 'react-syntax-highlighter/dist/cjs/styles/hljs/github' {
const style: any;
export default style;
}
type SupportedLanguages = 'markdown' | 'htmlbars' | 'sql' | 'json';
// For type checking when importing languages
function importLanguage<T extends SupportedLanguages>(language: T) {
return import(`react-syntax-highlighter/dist/cjs/languages/hljs/${language}`);
}

View File

@@ -46,3 +46,23 @@ test('Sort starts with first', async () => {
test('Sort same case first', async () => {
expect(['%f %B', '%F %b'].sort(searchSort('%F'))).toEqual(['%F %b', '%f %B']);
});
test('returns localeCompare result when no search term provided', () => {
expect(rankedSearchCompare('banana', 'apple', '')).toBeGreaterThan(0);
expect(rankedSearchCompare('apple', 'banana', '')).toBeLessThan(0);
});
test('handles empty string a', () => {
const result = rankedSearchCompare('', 'hello', 'hello');
expect(typeof result).toBe('number');
});
test('handles empty string b', () => {
const result = rankedSearchCompare('hello', '', 'hello');
expect(typeof result).toBe('number');
});
test('falls back to localeCompare when strings have no match relationship to search', () => {
expect(rankedSearchCompare('abc', 'def', 'xyz')).toBeLessThan(0);
expect(rankedSearchCompare('def', 'abc', 'xyz')).toBeGreaterThan(0);
});

View File

@@ -0,0 +1,39 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import withLabel from './withLabel';
test('withLabel returns false when validator passes', () => {
const validator = () => false as false;
const labeled = withLabel(validator, 'Field');
expect(labeled('any value')).toBe(false);
});
test('withLabel prepends label to validator error message', () => {
const validator = () => 'is required';
const labeled = withLabel(validator, 'Name');
expect(labeled('')).toBe('Name is required');
});
test('withLabel passes value and state to underlying validator', () => {
const validator = jest.fn(() => false as false);
const labeled = withLabel(validator, 'Field');
labeled('value', { someState: true });
expect(validator).toHaveBeenCalledWith('value', { someState: true });
});

View File

@@ -144,4 +144,10 @@ describe('SupersetClient', () => {
fetchMock.clearHistory().removeRoutes();
});
test('getCSRFToken() returns existing token when already configured', async () => {
SupersetClient.configure({ csrfToken: 'my_token' });
const token = await SupersetClient.getCSRFToken();
expect(token).toBe('my_token');
});
});

View File

@@ -108,6 +108,20 @@ describe('SupersetClientClass', () => {
expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(0);
});
test('getCSRFToken() returns existing csrfToken without fetching when already set', async () => {
const client = new SupersetClientClass({ csrfToken: 'existing_token' });
const token = await client.getCSRFToken();
expect(token).toBe('existing_token');
expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(0);
});
test('getCSRFToken() calls fetchCSRFToken when csrfToken is not set (line 261 || branch)', async () => {
const client = new SupersetClientClass({});
const token = await client.getCSRFToken();
expect(fetchMock.callHistory.calls(LOGIN_GLOB)).toHaveLength(1);
expect(token).toBe(1234);
});
test('calls api/v1/security/csrf_token/ when init(force=true) is called even if a CSRF token is passed', async () => {
expect.assertions(4);
const initialToken = 'initial_token';
@@ -156,6 +170,25 @@ describe('SupersetClientClass', () => {
}
});
test('does not set csrfToken when json response is a non-object primitive (line 245 false branch)', async () => {
expect.assertions(1);
fetchMock.removeRoute(LOGIN_GLOB);
// String '123' is used as raw body text; response.json() parses it to the
// number 123, so typeof json === 'object' is false
fetchMock.get(LOGIN_GLOB, '123', { name: LOGIN_GLOB });
let error;
try {
await new SupersetClientClass({}).init();
} catch (err) {
error = err;
} finally {
expect(error as typeof invalidCsrfTokenError).toEqual(
invalidCsrfTokenError,
);
}
});
test('does not set csrfToken if response is not json', async () => {
expect.assertions(1);
fetchMock.removeRoute(LOGIN_GLOB);

View File

@@ -187,3 +187,81 @@ test('CurrencyFormatter gracefully handles invalid currency code', () => {
// Should not throw, should return formatted value without currency symbol
expect(formatter.format(1000)).toBe('1,000.00');
});
test('CurrencyFormatter AUTO mode uses suffix position from row context', () => {
const formatter = new CurrencyFormatter({
currency: { symbol: 'AUTO', symbolPosition: 'suffix' },
d3Format: ',.2f',
});
const row = { currency: 'EUR' };
const result = formatter.format(1000, row, 'currency');
expect(result).toContain('€');
expect(result).toMatch(/1,000\.00.*€/);
});
test('CurrencyFormatter AUTO mode uses default suffix when symbolPosition is unknown', () => {
const formatter = new CurrencyFormatter({
// @ts-expect-error
currency: { symbol: 'AUTO' },
d3Format: ',.2f',
});
const row = { currency: 'EUR' };
const result = formatter.format(1000, row, 'currency');
expect(result).toContain('€');
expect(result).toMatch(/1,000\.00.*€/);
});
test('CurrencyFormatter AUTO mode returns plain value when row currency is not a string (line 52)', () => {
const formatter = new CurrencyFormatter({
currency: { symbol: 'AUTO', symbolPosition: 'prefix' },
d3Format: ',.2f',
});
// Passing a numeric currency value causes normalizeCurrency to hit
// `typeof value !== 'string'` → return null, so no symbol is appended
const row = { currency: 123 };
expect(formatter.format(1000, row as any, 'currency')).toBe('1,000.00');
});
test('CurrencyFormatter AUTO mode returns plain value when getCurrencySymbol returns undefined (line 126 false branch)', () => {
const formatter = new CurrencyFormatter({
currency: { symbol: 'AUTO', symbolPosition: 'prefix' },
d3Format: ',.2f',
});
const OrigNumberFormat = Intl.NumberFormat;
// Return formatToParts without a 'currency' entry so getCurrencySymbol → undefined
Intl.NumberFormat = jest.fn().mockImplementation(() => ({
formatToParts: () => [{ type: 'integer', value: '1' }],
})) as unknown as typeof Intl.NumberFormat;
const row = { currency: 'EUR' };
const result = formatter.format(1000, row, 'currency');
Intl.NumberFormat = OrigNumberFormat;
expect(result).toBe('1,000.00');
});
test('CurrencyFormatter AUTO mode falls back to plain value when getCurrencySymbol throws', () => {
const formatter = new CurrencyFormatter({
currency: { symbol: 'AUTO', symbolPosition: 'prefix' },
d3Format: ',.2f',
});
// Mock Intl.NumberFormat to throw to simulate an environment where the
// currency code is rejected, triggering the catch block in format()
const OrigNumberFormat = Intl.NumberFormat;
Intl.NumberFormat = jest.fn().mockImplementation(() => {
throw new RangeError('Invalid currency code');
}) as unknown as typeof Intl.NumberFormat;
const row = { currency: 'ZZZ' };
const result = formatter.format(1000, row, 'currency');
Intl.NumberFormat = OrigNumberFormat;
expect(result).toBe('1,000.00');
});

View File

@@ -27,6 +27,10 @@ import {
NumberFormatter,
ValueFormatter,
} from '@superset-ui/core';
import {
analyzeCurrencyInData,
resolveAutoCurrency,
} from '../../src/currency-format/utils';
test('buildCustomFormatters without saved metrics returns empty object', () => {
expect(
@@ -219,3 +223,192 @@ test('getValueFormatter return NumberFormatter when no currency formatters', ()
);
expect(formatter).toBeInstanceOf(NumberFormatter);
});
test('analyzeCurrencyInData returns null when all currency values are null/undefined', () => {
const data = [
{ value: 100, currency: null },
{ value: 200, currency: undefined },
];
expect(analyzeCurrencyInData(data as any, 'currency')).toBeNull();
});
test('analyzeCurrencyInData returns null when currencyColumn is not provided', () => {
expect(analyzeCurrencyInData([{ value: 100 }], undefined)).toBeNull();
});
test('analyzeCurrencyInData returns detected currency for consistent values', () => {
const data = [
{ value: 100, currency: 'USD' },
{ value: 200, currency: 'USD' },
];
expect(analyzeCurrencyInData(data, 'currency')).toBe('USD');
});
test('resolveAutoCurrency returns currencyFormat unchanged when not AUTO', () => {
const currency: Currency = { symbol: 'USD', symbolPosition: 'prefix' };
expect(resolveAutoCurrency(currency, null)).toEqual(currency);
});
test('resolveAutoCurrency returns currency from backendDetected when AUTO', () => {
const currency: Currency = { symbol: 'AUTO', symbolPosition: 'prefix' };
const result = resolveAutoCurrency(currency, 'EUR');
expect(result).toEqual({ symbol: 'EUR', symbolPosition: 'prefix' });
});
test('resolveAutoCurrency returns null when AUTO and no detection source', () => {
const currency: Currency = { symbol: 'AUTO', symbolPosition: 'prefix' };
expect(resolveAutoCurrency(currency, null)).toBeNull();
});
test('resolveAutoCurrency detects currency from data when backendDetected is undefined', () => {
const currency: Currency = { symbol: 'AUTO', symbolPosition: 'suffix' };
const data = [
{ value: 100, cur: 'JPY' },
{ value: 200, cur: 'JPY' },
];
const result = resolveAutoCurrency(currency, undefined, data, 'cur');
expect(result).toEqual({ symbol: 'JPY', symbolPosition: 'suffix' });
});
test('resolveAutoCurrency returns null when data analysis finds mixed currencies', () => {
const currency: Currency = { symbol: 'AUTO', symbolPosition: 'prefix' };
const data = [{ cur: 'USD' }, { cur: 'EUR' }];
const result = resolveAutoCurrency(currency, undefined, data, 'cur');
expect(result).toBeNull();
});
test('buildCustomFormatters with AUTO currency and data resolves currency', () => {
const data = [{ metric: 1, currency: 'EUR' }];
const result = buildCustomFormatters(
['metric'],
{},
{},
',.2f',
{ symbol: 'AUTO', symbolPosition: 'prefix' },
data,
'currency',
) as Record<string, ValueFormatter>;
expect(result).toHaveProperty('metric');
expect(result.metric).toBeInstanceOf(CurrencyFormatter);
});
test('buildCustomFormatters with AUTO currency and no detected currency returns NumberFormatter', () => {
// Mixed currencies → null resolved format → NumberFormatter
const data = [
{ metric: 1, currency: 'USD' },
{ metric: 2, currency: 'EUR' },
];
const result = buildCustomFormatters(
['metric'],
{},
{},
',.2f',
{ symbol: 'AUTO', symbolPosition: 'prefix' },
data,
'currency',
) as Record<string, ValueFormatter>;
expect(result).toHaveProperty('metric');
expect(result.metric).toBeInstanceOf(NumberFormatter);
});
test('getValueFormatter with AUTO currency and detectedCurrency provided', () => {
const formatter = getValueFormatter(
['count'],
{},
{},
',.1f',
{ symbol: 'AUTO', symbolPosition: 'prefix' },
'count',
undefined,
undefined,
'USD',
);
expect(formatter).toBeInstanceOf(CurrencyFormatter);
});
test('getValueFormatter with AUTO currency and null detectedCurrency returns NumberFormatter', () => {
const formatter = getValueFormatter(
['count'],
{},
{},
',.1f',
{ symbol: 'AUTO', symbolPosition: 'prefix' },
'count',
undefined,
undefined,
null,
);
expect(formatter).toBeInstanceOf(NumberFormatter);
});
test('getValueFormatter with AUTO currency and data + currencyCodeColumn', () => {
const data = [
{ count: 100, currency: 'GBP' },
{ count: 200, currency: 'GBP' },
];
const formatter = getValueFormatter(
['count'],
{},
{},
',.1f',
{ symbol: 'AUTO', symbolPosition: 'suffix' },
'count',
data,
'currency',
);
expect(formatter).toBeInstanceOf(CurrencyFormatter);
});
test('getValueFormatter with AUTO currency, data+column but mixed currencies falls back to NumberFormatter (line 178 false branch)', () => {
// Mixed currencies → analyzeCurrencyInData returns null → frontendDetected falsy
// → resolvedCurrencyFormat = null (the ternary false branch at line 178)
const data = [
{ count: 100, currency: 'USD' },
{ count: 200, currency: 'EUR' },
];
const formatter = getValueFormatter(
['count'],
{},
{},
',.1f',
{ symbol: 'AUTO', symbolPosition: 'prefix' },
'count',
data,
'currency',
);
expect(formatter).toBeInstanceOf(NumberFormatter);
});
test('getValueFormatter with AUTO currency and no data falls back to NumberFormatter', () => {
const formatter = getValueFormatter(
['count'],
{},
{},
',.1f',
{ symbol: 'AUTO', symbolPosition: 'prefix' },
'count',
);
expect(formatter).toBeInstanceOf(NumberFormatter);
});
test('getValueFormatter returns NumberFormatter via line 205 when AUTO resolves to null and metrics are all adhoc', () => {
// String metrics produce a NumberFormatter entry in buildCustomFormatters,
// making customFormatter truthy and bypassing line 205. Adhoc metric objects
// are skipped by buildCustomFormatters, so customFormatter stays undefined,
// and the resolvedCurrencyFormat === null branch at line 205 is reached.
const adhocMetric = {
expressionType: 'SIMPLE' as const,
aggregate: 'COUNT' as const,
column: { column_name: 'test' },
};
const formatter = getValueFormatter(
[adhocMetric],
{},
{},
',.1f',
{ symbol: 'AUTO', symbolPosition: 'prefix' },
'some_key',
undefined, // no data → else branch → resolvedCurrencyFormat = null
);
expect(formatter).toBeInstanceOf(NumberFormatter);
});

View File

@@ -18,6 +18,7 @@
*/
import {
isAdhocColumn,
isAdhocColumnReference,
isPhysicalColumn,
isQueryFormColumn,
} from '@superset-ui/core';
@@ -61,3 +62,16 @@ test('isQueryFormColumn returns true', () => {
test('isQueryFormColumn returns false', () => {
expect(isQueryFormColumn({})).toEqual(false);
});
test('isAdhocColumnReference returns true for adhoc column with isColumnReference', () => {
const ref = { ...adhocColumn, isColumnReference: true };
expect(isAdhocColumnReference(ref)).toEqual(true);
});
test('isAdhocColumnReference returns false for non-reference adhoc column', () => {
expect(isAdhocColumnReference(adhocColumn)).toEqual(false);
});
test('isAdhocColumnReference returns false for non-adhoc column', () => {
expect(isAdhocColumnReference('gender')).toEqual(false);
});

View File

@@ -28,6 +28,11 @@ import {
isAppliedNativeFilterType,
AppliedCrossFilterType,
AppliedNativeFilterType,
isChartCustomization,
isChartCustomizationDivider,
ChartCustomization,
ChartCustomizationDivider,
ChartCustomizationType,
} from '@superset-ui/core';
const filter: Filter = {
@@ -96,3 +101,31 @@ test('applied native filter type guard', () => {
expect(isAppliedNativeFilterType(appliedNativeFilter)).toBeTruthy();
expect(isAppliedNativeFilterType(appliedCrossFilter)).toBeFalsy();
});
const chartCustomization: ChartCustomization = {
id: 'custom_id',
type: ChartCustomizationType.ChartCustomization,
name: 'My Customization',
filterType: 'chart_customization',
targets: [],
scope: { rootPath: [], excluded: [] },
defaultDataMask: {},
controlValues: {},
};
const chartCustomizationDivider: ChartCustomizationDivider = {
id: 'divider_id',
type: ChartCustomizationType.Divider,
title: 'Divider',
description: 'A divider',
};
test('isChartCustomization type guard', () => {
expect(isChartCustomization(chartCustomization)).toBeTruthy();
expect(isChartCustomization(filter)).toBeFalsy();
});
test('isChartCustomizationDivider type guard', () => {
expect(isChartCustomizationDivider(chartCustomizationDivider)).toBeTruthy();
expect(isChartCustomizationDivider(chartCustomization)).toBeFalsy();
});