blob: 0acf0fd89ff0944df8dfd58af83a946f9b85e75f [file] [log] [blame]
/**
* 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 { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { SupersetClient } from '@superset-ui/core';
import MatrixifyDimensionControl, {
MatrixifyDimensionControlValue,
} from './MatrixifyDimensionControl';
import { fetchTopNValues } from './MatrixifyControl/utils/fetchTopNValues';
// Mock SupersetClient
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
SupersetClient: {
get: jest.fn(),
},
t: (str: string, ...args: any[]) => {
if (args.length > 0 && str.includes('%s')) {
return str.replace('%s', args[0]);
}
return str;
},
getColumnLabel: (col: string) => col,
}));
// Mock fetchTopNValues utility
jest.mock('./MatrixifyControl/utils/fetchTopNValues', () => ({
fetchTopNValues: jest.fn(),
extractDimensionValues: jest.fn(values => values.map((v: any) => v.value)),
}));
// Mock ControlHeader
jest.mock('src/explore/components/ControlHeader', () => ({
__esModule: true,
default: ({ label, description }: any) => (
<div data-testid="control-header">
{label && <span data-testid="label">{label}</span>}
{description && <span data-testid="description">{description}</span>}
</div>
),
}));
const mockDatasource = {
id: 1,
type: 'table',
columns: [
{ column_name: 'country', verbose_name: 'Country' },
{ column_name: 'region', verbose_name: 'Region' },
{ column_name: 'product', verbose_name: 'Product' },
],
filter_select: true,
};
const defaultProps = {
datasource: mockDatasource,
onChange: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
test('should render dimension selector with default label', () => {
render(<MatrixifyDimensionControl {...defaultProps} />);
expect(screen.getByText('Dimension')).toBeInTheDocument();
expect(screen.getAllByText('Select a dimension')).toHaveLength(2); // Header description + placeholder
expect(
screen.getByRole('combobox', { name: 'Select dimension' }),
).toBeInTheDocument();
});
test('should render with custom label and description', () => {
render(
<MatrixifyDimensionControl
{...defaultProps}
label="Custom Label"
description="Custom Description"
/>,
);
expect(screen.getByText('Custom Label')).toBeInTheDocument();
expect(screen.getByText('Custom Description')).toBeInTheDocument();
});
test('should display dimension options from datasource columns', async () => {
render(<MatrixifyDimensionControl {...defaultProps} />);
const select = screen.getByRole('combobox', { name: 'Select dimension' });
await userEvent.click(select);
await waitFor(() => {
expect(screen.getByText('country')).toBeInTheDocument();
expect(screen.getByText('region')).toBeInTheDocument();
expect(screen.getByText('product')).toBeInTheDocument();
});
});
test('should call onChange when dimension is selected', async () => {
const onChange = jest.fn();
render(<MatrixifyDimensionControl {...defaultProps} onChange={onChange} />);
const select = screen.getByRole('combobox', { name: 'Select dimension' });
await userEvent.click(select);
await userEvent.click(screen.getByText('country'));
expect(onChange).toHaveBeenCalledWith({
dimension: 'country',
values: [],
});
});
test('should display selected dimension value', () => {
const value: MatrixifyDimensionControlValue = {
dimension: 'country',
values: ['USA', 'Canada'],
};
render(<MatrixifyDimensionControl {...defaultProps} value={value} />);
// Check that the component renders with the selected dimension
expect(
screen.getByRole('combobox', { name: 'Select dimension' }),
).toBeInTheDocument();
// In members mode, the value selector should also appear
expect(
screen.getByRole('combobox', { name: 'Select dimension values' }),
).toBeInTheDocument();
});
test('should show value selector in members mode when dimension is selected', () => {
const value: MatrixifyDimensionControlValue = {
dimension: 'country',
values: [],
};
(SupersetClient.get as jest.Mock).mockResolvedValue({
json: { result: ['USA', 'Canada'] },
});
render(
<MatrixifyDimensionControl
{...defaultProps}
value={value}
selectionMode="members"
/>,
);
expect(
screen.getByRole('combobox', { name: 'Select dimension values' }),
).toBeInTheDocument();
});
test('should load dimension values from API in members mode', async () => {
const value: MatrixifyDimensionControlValue = {
dimension: 'country',
values: [],
};
(SupersetClient.get as jest.Mock).mockResolvedValue({
json: { result: ['USA', 'Canada', 'Mexico'] },
});
render(
<MatrixifyDimensionControl
{...defaultProps}
value={value}
selectionMode="members"
/>,
);
await waitFor(() => {
expect(SupersetClient.get).toHaveBeenCalledWith({
signal: expect.any(AbortSignal),
endpoint: '/api/v1/datasource/table/1/column/country/values/',
});
});
});
test('should handle API errors gracefully in members mode', async () => {
const value: MatrixifyDimensionControlValue = {
dimension: 'country',
values: [],
};
(SupersetClient.get as jest.Mock).mockRejectedValue(new Error('API Error'));
render(
<MatrixifyDimensionControl
{...defaultProps}
value={value}
selectionMode="members"
/>,
);
await waitFor(() => {
expect(SupersetClient.get).toHaveBeenCalled();
});
});
test('should not show value selector in topn mode', () => {
const value: MatrixifyDimensionControlValue = {
dimension: 'country',
values: [],
};
render(
<MatrixifyDimensionControl
{...defaultProps}
value={value}
selectionMode="topn"
/>,
);
expect(
screen.queryByRole('combobox', { name: 'Select dimension values' }),
).not.toBeInTheDocument();
});
test('should fetch TopN values when all params are provided', async () => {
const mockFetchTopNValues = fetchTopNValues as jest.MockedFunction<
typeof fetchTopNValues
>;
const onChange = jest.fn();
const value: MatrixifyDimensionControlValue = {
dimension: 'country',
values: [],
};
mockFetchTopNValues.mockResolvedValue([
{ value: 'USA', metricValue: 1000 },
{ value: 'Canada', metricValue: 800 },
]);
render(
<MatrixifyDimensionControl
{...defaultProps}
value={value}
onChange={onChange}
selectionMode="topn"
topNMetric="revenue"
topNValue={2}
topNOrder="DESC"
/>,
);
await waitFor(() => {
expect(mockFetchTopNValues).toHaveBeenCalledWith({
datasource: '1__table',
column: 'country',
metric: 'revenue',
limit: 2,
sortAscending: false,
filters: [],
timeRange: undefined,
});
});
});
test('should display error when TopN fetch fails', async () => {
const mockFetchTopNValues = fetchTopNValues as jest.MockedFunction<
typeof fetchTopNValues
>;
const value: MatrixifyDimensionControlValue = {
dimension: 'country',
values: [],
};
mockFetchTopNValues.mockRejectedValue(new Error('Fetch failed'));
render(
<MatrixifyDimensionControl
{...defaultProps}
value={value}
selectionMode="topn"
topNMetric="revenue"
topNValue={5}
/>,
);
await waitFor(() => {
expect(screen.getByText('Error: Fetch failed')).toBeInTheDocument();
});
});
test('should convert string topNValue to number', async () => {
const mockFetchTopNValues = fetchTopNValues as jest.MockedFunction<
typeof fetchTopNValues
>;
const value: MatrixifyDimensionControlValue = {
dimension: 'country',
values: [],
};
mockFetchTopNValues.mockResolvedValue([]);
render(
<MatrixifyDimensionControl
{...defaultProps}
value={value}
selectionMode="topn"
topNMetric="revenue"
topNValue={'10' as any} // String instead of number
topNOrder="ASC"
/>,
);
await waitFor(() => {
expect(fetchTopNValues).toHaveBeenCalledWith(
expect.objectContaining({
limit: 10, // Should be converted to number
sortAscending: true,
}),
);
});
});
test('should not load values for datasource without filter_select', () => {
const datasourceNoFilter = {
...mockDatasource,
filter_select: false,
};
const value: MatrixifyDimensionControlValue = {
dimension: 'country',
values: [],
};
render(
<MatrixifyDimensionControl
{...defaultProps}
datasource={datasourceNoFilter}
value={value}
selectionMode="members"
/>,
);
expect(SupersetClient.get).not.toHaveBeenCalled();
});
test('should handle empty dimension value', () => {
const value: MatrixifyDimensionControlValue = {
dimension: '',
values: [],
};
render(<MatrixifyDimensionControl {...defaultProps} value={value} />);
expect(
screen.queryByRole('combobox', { name: 'Select dimension values' }),
).not.toBeInTheDocument();
});
test('should handle undefined value prop', () => {
render(<MatrixifyDimensionControl {...defaultProps} value={undefined} />);
expect(
screen.getByRole('combobox', { name: 'Select dimension' }),
).toBeInTheDocument();
expect(
screen.queryByRole('combobox', { name: 'Select dimension values' }),
).not.toBeInTheDocument();
});
test('should handle datasources without columns', () => {
const datasourceWithoutColumns = {
...mockDatasource,
columns: [],
};
render(
<MatrixifyDimensionControl
{...defaultProps}
datasource={datasourceWithoutColumns}
/>,
);
expect(
screen.getByRole('combobox', { name: 'Select dimension' }),
).toBeInTheDocument();
});
test('should clear values when switching from topn to members mode', async () => {
const onChange = jest.fn();
const value: MatrixifyDimensionControlValue = {
dimension: 'country',
values: ['USA', 'Canada'],
};
const { rerender } = render(
<MatrixifyDimensionControl
{...defaultProps}
value={value}
onChange={onChange}
selectionMode="topn"
/>,
);
rerender(
<MatrixifyDimensionControl
{...defaultProps}
value={value}
onChange={onChange}
selectionMode="members"
/>,
);
await waitFor(() => {
expect(onChange).toHaveBeenCalledWith({
dimension: 'country',
values: [],
topNValues: [],
});
});
});