mirror of
https://github.com/apache/superset.git
synced 2026-04-07 18:35:15 +00:00
fix(superset-ui-core): achieve 100% coverage for npm run core:cover (#38397)
This commit is contained in:
committed by
GitHub
parent
35d0aad854
commit
69732d9dca
@@ -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']);
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user