This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 69732d9dca fix(superset-ui-core): achieve 100% coverage for npm run
core:cover (#38397)
69732d9dca is described below
commit 69732d9dca662a415e020488be98f5bacbb7de76
Author: Michael S. Molina <[email protected]>
AuthorDate: Wed Mar 4 13:56:51 2026 -0300
fix(superset-ui-core): achieve 100% coverage for npm run core:cover (#38397)
---
.../src/utils/metricColumnFilter.test.ts | 8 +
.../test/utils/getColorFormatters.test.ts | 41 ++
.../Matrixify/MatrixifyGridGenerator.test.ts | 120 +++++
.../components/Matrixify/MatrixifyGridGenerator.ts | 8 +-
.../types/matrixify.mocks.test.ts} | 20 +-
.../src/chart/types/matrixify.test.ts | 82 +++
.../superset-ui-core/src/chart/types/matrixify.ts | 36 +-
.../AsyncAceEditor/useJsonValidation.test.ts | 25 +
.../src/components/List/List.test.tsx | 10 +
.../src/components/Select/constants.test.ts | 49 ++
.../Table/utils/InteractiveTableUtils.test.ts | 574 +++++++++++++++++++++
.../src/types/react-syntax-highlighter.d.ts | 7 -
.../src/utils/rankedSearchCompare.test.ts | 20 +
.../List/List.test.tsx => utils/withLabel.test.ts} | 33 +-
.../test/connection/SupersetClient.test.ts | 6 +
.../test/connection/SupersetClientClass.test.ts | 33 ++
.../test/currency-format/CurrencyFormatter.test.ts | 78 +++
.../test/currency-format/utils.test.ts | 193 +++++++
.../test/query/types/Column.test.ts | 14 +
.../test/query/types/Dashboard.test.ts | 33 ++
20 files changed, 1328 insertions(+), 62 deletions(-)
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/metricColumnFilter.test.ts
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/metricColumnFilter.test.ts
index f6202263a2..a81ea54a62 100644
---
a/superset-frontend/packages/superset-ui-chart-controls/src/utils/metricColumnFilter.test.ts
+++
b/superset-frontend/packages/superset-ui-chart-controls/src/utils/metricColumnFilter.test.ts
@@ -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']);
diff --git
a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
index 0c0563049f..16e4feeb3e 100644
---
a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
+++
b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
@@ -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 = [
{
diff --git
a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.test.ts
b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.test.ts
index 5a66173718..a2915fef43 100644
---
a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.test.ts
+++
b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.test.ts
@@ -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',
diff --git
a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.ts
b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.ts
index 7895bf23a2..cc12d396b9 100644
---
a/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.ts
+++
b/superset-frontend/packages/superset-ui-core/src/chart/components/Matrixify/MatrixifyGridGenerator.ts
@@ -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
diff --git
a/superset-frontend/packages/superset-ui-core/src/types/react-syntax-highlighter.d.ts
b/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.mocks.test.ts
similarity index 60%
copy from
superset-frontend/packages/superset-ui-core/src/types/react-syntax-highlighter.d.ts
copy to
superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.mocks.test.ts
index f0860387dd..3fd4d0160d 100644
---
a/superset-frontend/packages/superset-ui-core/src/types/react-syntax-highlighter.d.ts
+++
b/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.mocks.test.ts
@@ -16,19 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
-declare module 'react-syntax-highlighter/dist/cjs/light' {
- import SyntaxHighlighter from 'react-syntax-highlighter';
- export default SyntaxHighlighter;
-}
-declare module 'react-syntax-highlighter/dist/cjs/styles/hljs/github' {
- const style: any;
- export default style;
-}
+import { isMatrixifyEnabled, MatrixifyGridRenderer } from './matrixify.mocks';
-type SupportedLanguages = 'markdown' | 'htmlbars' | 'sql' | 'json';
+test('isMatrixifyEnabled mock returns false by default', () => {
+ expect(isMatrixifyEnabled()).toBe(false);
+});
-// For type checking when importing languages
-function importLanguage<T extends SupportedLanguages>(language: T) {
- return
import(`react-syntax-highlighter/dist/cjs/languages/hljs/${language}`);
-}
+test('MatrixifyGridRenderer mock returns null by default', () => {
+ expect(MatrixifyGridRenderer()).toBeNull();
+});
diff --git
a/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.test.ts
b/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.test.ts
index 3ac8fa4b0a..6fe052a5a7 100644
---
a/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.test.ts
+++
b/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.test.ts
@@ -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',
diff --git
a/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.ts
b/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.ts
index e90a6b3a87..8c01651ca6 100644
--- a/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.ts
+++ b/superset-frontend/packages/superset-ui-core/src/chart/types/matrixify.ts
@@ -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;
diff --git
a/superset-frontend/packages/superset-ui-core/src/components/AsyncAceEditor/useJsonValidation.test.ts
b/superset-frontend/packages/superset-ui-core/src/components/AsyncAceEditor/useJsonValidation.test.ts
index bab4cc313d..e9179b4ce7 100644
---
a/superset-frontend/packages/superset-ui-core/src/components/AsyncAceEditor/useJsonValidation.test.ts
+++
b/superset-frontend/packages/superset-ui-core/src/components/AsyncAceEditor/useJsonValidation.test.ts
@@ -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)
+ });
});
diff --git
a/superset-frontend/packages/superset-ui-core/src/components/List/List.test.tsx
b/superset-frontend/packages/superset-ui-core/src/components/List/List.test.tsx
index 2423bc3a00..3c3c53dad4 100644
---
a/superset-frontend/packages/superset-ui-core/src/components/List/List.test.tsx
+++
b/superset-frontend/packages/superset-ui-core/src/components/List/List.test.tsx
@@ -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();
+});
diff --git
a/superset-frontend/packages/superset-ui-core/src/components/Select/constants.test.ts
b/superset-frontend/packages/superset-ui-core/src/components/Select/constants.test.ts
new file mode 100644
index 0000000000..78882c8215
--- /dev/null
+++
b/superset-frontend/packages/superset-ui-core/src/components/Select/constants.test.ts
@@ -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);
+});
diff --git
a/superset-frontend/packages/superset-ui-core/src/components/Table/utils/InteractiveTableUtils.test.ts
b/superset-frontend/packages/superset-ui-core/src/components/Table/utils/InteractiveTableUtils.test.ts
new file mode 100644
index 0000000000..827a87a600
--- /dev/null
+++
b/superset-frontend/packages/superset-ui-core/src/components/Table/utils/InteractiveTableUtils.test.ts
@@ -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);
+});
diff --git
a/superset-frontend/packages/superset-ui-core/src/types/react-syntax-highlighter.d.ts
b/superset-frontend/packages/superset-ui-core/src/types/react-syntax-highlighter.d.ts
index f0860387dd..ad0705a20b 100644
---
a/superset-frontend/packages/superset-ui-core/src/types/react-syntax-highlighter.d.ts
+++
b/superset-frontend/packages/superset-ui-core/src/types/react-syntax-highlighter.d.ts
@@ -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}`);
-}
diff --git
a/superset-frontend/packages/superset-ui-core/src/utils/rankedSearchCompare.test.ts
b/superset-frontend/packages/superset-ui-core/src/utils/rankedSearchCompare.test.ts
index 268511a5cb..a72f3e2502 100644
---
a/superset-frontend/packages/superset-ui-core/src/utils/rankedSearchCompare.test.ts
+++
b/superset-frontend/packages/superset-ui-core/src/utils/rankedSearchCompare.test.ts
@@ -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);
+});
diff --git
a/superset-frontend/packages/superset-ui-core/src/components/List/List.test.tsx
b/superset-frontend/packages/superset-ui-core/src/utils/withLabel.test.ts
similarity index 52%
copy from
superset-frontend/packages/superset-ui-core/src/components/List/List.test.tsx
copy to superset-frontend/packages/superset-ui-core/src/utils/withLabel.test.ts
index 2423bc3a00..67e51eb2c9 100644
---
a/superset-frontend/packages/superset-ui-core/src/components/List/List.test.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/utils/withLabel.test.ts
@@ -16,27 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { render, screen } from '@superset-ui/core/spec';
-import { List } from '.';
-import type { ListProps } from './types';
-const mockedProps: ListProps<any> = {
- dataSource: ['Item 1', 'Item 2', 'Item 3'],
- renderItem: item => <div>{item}</div>,
-};
+import withLabel from './withLabel';
-test('should render', () => {
- const { container } = render(<List {...mockedProps} />);
- expect(container).toBeInTheDocument();
+test('withLabel returns false when validator passes', () => {
+ const validator = () => false as false;
+ const labeled = withLabel(validator, 'Field');
+ expect(labeled('any value')).toBe(false);
});
-test('should render the correct number of items', () => {
- render(<List {...mockedProps} />);
-
- const listItemElements = screen.getAllByText(/Item \d/);
+test('withLabel prepends label to validator error message', () => {
+ const validator = () => 'is required';
+ const labeled = withLabel(validator, 'Name');
+ expect(labeled('')).toBe('Name is required');
+});
- expect(listItemElements.length).toBe(3);
- listItemElements.forEach((item, index) => {
- expect(item).toHaveTextContent(`Item ${index + 1}`);
- });
+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 });
});
diff --git
a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts
b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts
index 92efac6089..cdfe2dacd9 100644
---
a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts
+++
b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts
@@ -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');
+ });
});
diff --git
a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
index e5020b2081..bc9448be4e 100644
---
a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
+++
b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts
@@ -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);
diff --git
a/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts
b/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts
index 251ecdded0..1b27d0d0d7 100644
---
a/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts
+++
b/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts
@@ -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');
+});
diff --git
a/superset-frontend/packages/superset-ui-core/test/currency-format/utils.test.ts
b/superset-frontend/packages/superset-ui-core/test/currency-format/utils.test.ts
index ef8f0ee3d1..facf718380 100644
---
a/superset-frontend/packages/superset-ui-core/test/currency-format/utils.test.ts
+++
b/superset-frontend/packages/superset-ui-core/test/currency-format/utils.test.ts
@@ -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);
+});
diff --git
a/superset-frontend/packages/superset-ui-core/test/query/types/Column.test.ts
b/superset-frontend/packages/superset-ui-core/test/query/types/Column.test.ts
index d4391cfd01..e940802197 100644
---
a/superset-frontend/packages/superset-ui-core/test/query/types/Column.test.ts
+++
b/superset-frontend/packages/superset-ui-core/test/query/types/Column.test.ts
@@ -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);
+});
diff --git
a/superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts
b/superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts
index c1c7143957..b34a417420 100644
---
a/superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts
+++
b/superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts
@@ -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();
+});